From 43a6cc376b0034c2a3891f7b28a35200b8c5c40d Mon Sep 17 00:00:00 2001 From: John McCardle Date: Tue, 14 Jul 2026 18:21:19 -0400 Subject: [PATCH 1/5] fix(engine): UTF-8 filesystem encoding, --run-forever in --help, honest step() docs Three documentation-adjacent defects, found while auditing what the July bugfix batch obliged the docs to say. #378 -- init_python_with_config(), the init path main.cpp actually uses, did no pre-initialization at all. PyPreConfig.utf8_mode was never enabled, so the filesystem encoding fell back to ASCII while sys.getdefaultencoding() and the locale both reported UTF-8. open() with no explicit encoding= therefore could not read a UTF-8 file: open("notes.py").read() # UnicodeDecodeError: 'ascii' codec can't decode byte 0xc2 In any normal CPython 3, open() defaults to UTF-8. This broke any script reading a data file, a save file, or its own source. init_python() -- the other init path -- had always set utf8_mode = 1; only the live path was missing it. This also corrects the long-standing folklore that "--exec scripts must be ASCII-only". They need not be, and never did: the C++ side reads the file and hands the bytes to Python, which parses them as UTF-8 per PEP 3120. The folklore was pointing at open(), via harnesses that read scripts themselves. --run-forever (#350) parsed but was absent from print_help(), which lists every other McRogueFace flag. The only place a user could learn it existed was the runtime error printed after their script had already failed. CLI flags are not in the API manifest, so nothing flagged the omission. mcrfpy.step()'s docstring still read "Advance simulation time" -- accurate until 30abb0b made step() a full simulation frame (scene update, Python Scene.update(), timers, animations, transition completion, frame metrics, currentFrame++). It now says so, says why render and input are deliberately excluded, and states the sys.exit() requirement that --run-forever exists to waive. The most-changed semantic in the batch had the least-changed documentation. closes #378 --- api/manifest.json | 2 +- src/CommandLineParser.cpp | 2 + src/McRFPy_API.cpp | 29 ++++++- .../regression/issue_378_utf8_source_test.py | 85 +++++++++++++++++++ 4 files changed, 115 insertions(+), 3 deletions(-) create mode 100644 tests/regression/issue_378_utf8_source_test.py diff --git a/api/manifest.json b/api/manifest.json index d86e7d0..79666b5 100644 --- a/api/manifest.json +++ b/api/manifest.json @@ -1 +1 @@ -{"commit":"5323428","dirty":true,"functions":{"bresenham":{"doc_sha":"ee2d0f7a54a664f5","kind":"function","lifecycle":{"since":"0.2.8"},"signature":"bresenham(start, end, include_start=True, include_end=True) -> list[tuple[int, int]]"},"end_benchmark":{"doc_sha":"5d783c5dabb7a4e9","kind":"function","lifecycle":{"since":"0.2.8"},"signature":"end_benchmark() -> str"},"exit":{"doc_sha":"7572d4d6b1a1c8a4","kind":"function","lifecycle":{"since":"0.2.8"},"signature":"exit() -> None"},"find":{"doc_sha":"364b95807edcf8b4","kind":"function","lifecycle":{"since":"0.2.8"},"signature":"find(name: str, scene: str = None) -> UIDrawable | None"},"find_all":{"doc_sha":"b4a278f16b172131","kind":"function","lifecycle":{"since":"0.2.8"},"signature":"find_all(pattern: str, scene: str = None) -> list"},"get_metrics":{"doc_sha":"a0bbee3858baa4a9","kind":"function","lifecycle":{"modified":{"commit":"464af6a","version":"0.2.8-dev"},"since":"0.2.8"},"signature":"get_metrics() -> dict"},"lock":{"doc_sha":"e42989cabc81d080","kind":"function","lifecycle":{"since":"0.2.8"},"signature":"lock() -> _LockContext"},"log_benchmark":{"doc_sha":"4564bf7321366a0a","kind":"function","lifecycle":{"since":"0.2.8"},"signature":"log_benchmark(message: str) -> None"},"set_dev_console":{"doc_sha":"940ed19ff4f5243f","kind":"function","lifecycle":{"since":"0.2.8"},"signature":"set_dev_console(enabled: bool) -> None"},"set_scale":{"doc_sha":"b565b441a326af48","kind":"function","lifecycle":{"since":"0.2.8"},"signature":"set_scale(multiplier: float) -> None"},"start_benchmark":{"doc_sha":"783957f3517518fe","kind":"function","lifecycle":{"since":"0.2.8"},"signature":"start_benchmark() -> None"},"step":{"doc_sha":"90dec8ed10ee644f","kind":"function","lifecycle":{"since":"0.2.8"},"signature":"step(dt: float = None) -> float"}},"module_attributes":{"animations":{"doc_sha":"30d25725feac9eb6","kind":"attribute","lifecycle":{"since":"0.2.8-dev"},"readonly":true},"current_scene":{"doc_sha":"ab496ea99fec2fa8","kind":"attribute","lifecycle":{"since":"0.2.8-dev"},"readonly":false},"default_transition":{"doc_sha":"73fb568e9cb25540","kind":"attribute","lifecycle":{"since":"0.2.8-dev"},"readonly":false},"default_transition_duration":{"doc_sha":"ae57530eded49138","kind":"attribute","lifecycle":{"since":"0.2.8-dev"},"readonly":false},"save_dir":{"doc_sha":"96ffa5a27d178b14","kind":"attribute","lifecycle":{"since":"0.2.8-dev"},"readonly":true},"scenes":{"doc_sha":"8021c54383d3cb8e","kind":"attribute","lifecycle":{"since":"0.2.8-dev"},"readonly":true},"timers":{"doc_sha":"48e2aa7747d8f527","kind":"attribute","lifecycle":{"since":"0.2.8-dev"},"readonly":true}},"objects":{"AStarPath":{"doc_sha":"60dd12bb4a54f3dd","kind":"class","lifecycle":{"since":"0.2.8"},"members":{"destination":{"doc_sha":"62a81c7267ad9ed9","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"origin":{"doc_sha":"c7dd130b8d4ef800","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"peek":{"doc_sha":"14054f10b4d9dda0","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"peek() -> Vector"},"remaining":{"doc_sha":"c193adc4bbfa3564","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"walk":{"doc_sha":"3fb64b239109d301","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"walk() -> Vector"}}},"Alignment":{"doc_sha":"77d93d03c8994f84","kind":"class","lifecycle":{"since":"0.2.8"},"members":{"as_integer_ratio":{"doc_sha":"3eb115fab0d0e0ec","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"($self, /)"},"bit_count":{"doc_sha":"f276da700b5a2e37","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"($self, /)"},"bit_length":{"doc_sha":"15b29db62fd0fa6c","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"($self, /)"},"conjugate":{"doc_sha":"6517a002faad7c75","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"($self, /)"},"denominator":{"doc_sha":"5051eb1570768851","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"from_bytes":{"doc_sha":"64672607865643a7","kind":"classmethod","lifecycle":{"since":"0.2.8"},"signature":"($type, /, bytes, byteorder='big', *, signed=False)"},"imag":{"doc_sha":"ef78ea410df39b26","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"is_integer":{"doc_sha":"6176763fbac2fd22","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"($self, /)"},"numerator":{"doc_sha":"ed255748ce2eda7c","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"real":{"doc_sha":"d44497f1990af6fe","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"to_bytes":{"doc_sha":"b452319e48e64764","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"($self, /, length=1, byteorder='big', *, signed=False)"}}},"Arc":{"doc_sha":"8323f4b7101509e9","kind":"class","lifecycle":{"since":"0.2.8"},"members":{"align":{"doc_sha":"58657c3cfa010f31","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"animate":{"doc_sha":"400df4f5893261c5","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"animate(property: str, target: Any, duration: float, easing=None, delta=False, loop=False, callback=None, conflict_mode='replace') -> Animation"},"bounds":{"doc_sha":"d3186ac3b3ff8bed","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"center":{"doc_sha":"abf1643dda106bf8","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"color":{"doc_sha":"bdf932405b56c319","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"end_angle":{"doc_sha":"74f9704909a3a23d","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"global_bounds":{"doc_sha":"aa4de9a20df55c2b","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"global_position":{"doc_sha":"b8dabbecc9fccd2a","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"grid_pos":{"doc_sha":"9df77e9a996208ba","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"grid_size":{"doc_sha":"e32208817299dfe9","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"horiz_margin":{"doc_sha":"0ef227a4cae6e675","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"hovered":{"doc_sha":"af0d353ce08f7be0","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"margin":{"doc_sha":"ab774a7da8cfe9bf","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"move":{"doc_sha":"5c74f429970a2d2b","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"move(dx, dy) or (delta) -> None"},"name":{"doc_sha":"0cc0684f26d8a660","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"on_click":{"doc_sha":"f2ef657aaf8b6c68","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"on_enter":{"doc_sha":"48c996802e262396","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"on_exit":{"doc_sha":"c14ef0d6d6d40b0e","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"on_move":{"doc_sha":"9c3e26818b5b25b0","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"opacity":{"doc_sha":"c1a7ef41e074519e","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"origin":{"doc_sha":"270bdf366c74bb1d","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"parent":{"doc_sha":"171b0de4e295c55b","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"pos":{"doc_sha":"6612fa35c43918e7","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"radius":{"doc_sha":"f9c2310b775f1e5a","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"realign":{"doc_sha":"8dfa7333a49aa697","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"realign() -> None"},"resize":{"doc_sha":"005a04f1d129ebe5","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"resize(width, height) or (size) -> None"},"rotate_with_camera":{"doc_sha":"5c3af880218d3a08","kind":"property","lifecycle":{"modified":{"commit":"4bb1966","version":"0.2.8-dev"},"since":"0.2.8"},"signature":""},"rotation":{"doc_sha":"61ba3e66a0841a08","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"start_angle":{"doc_sha":"6b91f175ebe5ecb4","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"thickness":{"doc_sha":"f8eba89682554e6d","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"vert_margin":{"doc_sha":"e2042be923bc6fcb","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"visible":{"doc_sha":"49221a4ae830f3b8","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"z_index":{"doc_sha":"67dd517aa153c158","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""}}},"AutoRuleSet":{"doc_sha":"49ab716632a44f06","kind":"class","lifecycle":{"since":"0.2.8"},"members":{"apply":{"doc_sha":"701b6f038cfd6809","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"apply(discrete_map: DiscreteMap, tile_layer: TileLayer, seed: int = 0) -> None"},"grid_size":{"doc_sha":"570f4c81acb781d8","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"group_count":{"doc_sha":"892fc283939b3e0c","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"name":{"doc_sha":"4f5ca90860649b64","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"resolve":{"doc_sha":"f8fff4e6e6488e3f","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"resolve(discrete_map: DiscreteMap, seed: int = 0) -> list[int]"},"rule_count":{"doc_sha":"9b9d5a7c1475dcfa","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"terrain_enum":{"doc_sha":"eb365bf99bbcca37","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"terrain_enum() -> IntEnum"},"value_count":{"doc_sha":"3467d158dd5af073","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"values":{"doc_sha":"b3199e059fd1ca2a","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""}}},"BSP":{"doc_sha":"7e971e0542cf4006","kind":"class","lifecycle":{"since":"0.2.8"},"members":{"adjacency":{"doc_sha":"9373aa85408486ba","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"bounds":{"doc_sha":"9c41e7403f2a0b2f","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"clear":{"doc_sha":"2e4ffd3360ac858e","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"clear() -> BSP"},"find":{"doc_sha":"fccfac484a2b889c","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"find(pos: tuple[int, int] | list | Vector) -> BSPNode | None"},"get_leaf":{"doc_sha":"d8ba680ec2ad4fd1","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"get_leaf(index: int) -> BSPNode"},"leaves":{"doc_sha":"439ec78ec02e6df8","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"leaves() -> Iterator[BSPNode]"},"pos":{"doc_sha":"b8b27f4ad76e2870","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"root":{"doc_sha":"d1e3d9741dab383d","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"size":{"doc_sha":"621f11f9a37dd89d","kind":"property","lifecycle":{"since":"0.2.8"},"signature":"Dimensions (width, height). Read-only."},"split_once":{"doc_sha":"0d7c0c33156bc4e5","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"split_once(horizontal: bool, position: int) -> BSP"},"split_recursive":{"doc_sha":"fa516b79833e5094","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"split_recursive(depth: int, min_size: tuple[int, int], max_ratio: float = 1.5, seed: int | None = None) -> BSP"},"to_heightmap":{"doc_sha":"a0f03cab7a0f2d1b","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"to_heightmap(size: tuple[int, int] | None = None, select: str = 'leaves', shrink: int = 0, value: float = 1.0) -> HeightMap"},"traverse":{"doc_sha":"57da83e0f37686aa","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"traverse(order: Traversal = Traversal.LEVEL_ORDER) -> Iterator[BSPNode]"}}},"Behavior":{"doc_sha":"09206365e6c37ecc","kind":"class","lifecycle":{"since":"0.2.8"},"members":{"as_integer_ratio":{"doc_sha":"3eb115fab0d0e0ec","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"($self, /)"},"bit_count":{"doc_sha":"f276da700b5a2e37","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"($self, /)"},"bit_length":{"doc_sha":"15b29db62fd0fa6c","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"($self, /)"},"conjugate":{"doc_sha":"6517a002faad7c75","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"($self, /)"},"denominator":{"doc_sha":"5051eb1570768851","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"from_bytes":{"doc_sha":"64672607865643a7","kind":"classmethod","lifecycle":{"since":"0.2.8"},"signature":"($type, /, bytes, byteorder='big', *, signed=False)"},"imag":{"doc_sha":"ef78ea410df39b26","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"is_integer":{"doc_sha":"6176763fbac2fd22","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"($self, /)"},"numerator":{"doc_sha":"ed255748ce2eda7c","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"real":{"doc_sha":"d44497f1990af6fe","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"to_bytes":{"doc_sha":"b452319e48e64764","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"($self, /, length=1, byteorder='big', *, signed=False)"}}},"Billboard":{"doc_sha":"38614d1d19942bab","kind":"class","lifecycle":{"since":"0.2.8"},"members":{"facing":{"doc_sha":"f82e2d7d8087804a","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"opacity":{"doc_sha":"dc5ed39915ab896c","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"phi":{"doc_sha":"1450c89fb5b47929","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"pos":{"doc_sha":"0d5fec7fd4701ea6","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"scale":{"doc_sha":"c9d55c4579681854","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"sprite_index":{"doc_sha":"170c20c841b586fc","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"texture":{"doc_sha":"b6d846566b6ccf93","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"theta":{"doc_sha":"c214ecbcb099228b","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"visible":{"doc_sha":"adf8070be9a09eba","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""}}},"CallableBinding":{"doc_sha":"711bf0f149ccae7a","kind":"class","lifecycle":{"since":"0.2.8"},"members":{"callable":{"doc_sha":"96d3d31a6b7c56d9","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"is_valid":{"doc_sha":"bc69c90e13475ae6","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"value":{"doc_sha":"8f7d9eb4f520c017","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""}}},"Caption":{"doc_sha":"8cd46324baa46a0d","kind":"class","lifecycle":{"since":"0.2.8"},"members":{"align":{"doc_sha":"58657c3cfa010f31","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"animate":{"doc_sha":"400df4f5893261c5","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"animate(property: str, target: Any, duration: float, easing=None, delta=False, loop=False, callback=None, conflict_mode='replace') -> Animation"},"bounds":{"doc_sha":"d3186ac3b3ff8bed","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"fill_color":{"doc_sha":"af7ba90df95e4b79","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"font":{"doc_sha":"fbdafdb33921aa13","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"font_size":{"doc_sha":"7894149ec4efec6e","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"global_bounds":{"doc_sha":"aa4de9a20df55c2b","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"global_position":{"doc_sha":"b8dabbecc9fccd2a","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"grid_pos":{"doc_sha":"79a7d48503e657d1","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"grid_size":{"doc_sha":"25b029c86a618fee","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"h":{"doc_sha":"ca1a98d700ff1888","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"horiz_margin":{"doc_sha":"0ef227a4cae6e675","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"hovered":{"doc_sha":"af0d353ce08f7be0","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"margin":{"doc_sha":"ab774a7da8cfe9bf","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"move":{"doc_sha":"5c74f429970a2d2b","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"move(dx, dy) or (delta) -> None"},"name":{"doc_sha":"90a2ab0ec07c00eb","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"on_click":{"doc_sha":"b3b8de4e294d71b6","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"on_enter":{"doc_sha":"48c996802e262396","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"on_exit":{"doc_sha":"c14ef0d6d6d40b0e","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"on_move":{"doc_sha":"9c3e26818b5b25b0","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"opacity":{"doc_sha":"c1a7ef41e074519e","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"origin":{"doc_sha":"270bdf366c74bb1d","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"outline":{"doc_sha":"6580b31092048bcb","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"outline_color":{"doc_sha":"388919803d270fd3","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"parent":{"doc_sha":"171b0de4e295c55b","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"pos":{"doc_sha":"44e4967e15212959","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"realign":{"doc_sha":"8dfa7333a49aa697","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"realign() -> None"},"resize":{"doc_sha":"005a04f1d129ebe5","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"resize(width, height) or (size) -> None"},"rotate_with_camera":{"doc_sha":"5c3af880218d3a08","kind":"property","lifecycle":{"modified":{"commit":"4bb1966","version":"0.2.8-dev"},"since":"0.2.8"},"signature":""},"rotation":{"doc_sha":"61ba3e66a0841a08","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"shader":{"doc_sha":"418952064319ffb5","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"size":{"doc_sha":"85abb564299b703d","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"text":{"doc_sha":"41628142b287defe","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"uniforms":{"doc_sha":"b1f2699da618d002","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"vert_margin":{"doc_sha":"e2042be923bc6fcb","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"visible":{"doc_sha":"49221a4ae830f3b8","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"w":{"doc_sha":"8472810bca891509","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"x":{"doc_sha":"528487f8deaaf789","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"y":{"doc_sha":"114ecec940477107","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"z_index":{"doc_sha":"328d87ae44d9fb71","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""}}},"Circle":{"doc_sha":"077981a1fbdfb6e3","kind":"class","lifecycle":{"since":"0.2.8"},"members":{"align":{"doc_sha":"58657c3cfa010f31","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"animate":{"doc_sha":"400df4f5893261c5","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"animate(property: str, target: Any, duration: float, easing=None, delta=False, loop=False, callback=None, conflict_mode='replace') -> Animation"},"bounds":{"doc_sha":"d3186ac3b3ff8bed","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"center":{"doc_sha":"d1e80ccd6bf706b9","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"fill_color":{"doc_sha":"46be64336e296c50","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"global_bounds":{"doc_sha":"aa4de9a20df55c2b","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"global_position":{"doc_sha":"b8dabbecc9fccd2a","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"grid_pos":{"doc_sha":"20bebfa7432ed339","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"grid_size":{"doc_sha":"b43e33c2d49a3565","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"horiz_margin":{"doc_sha":"0ef227a4cae6e675","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"hovered":{"doc_sha":"af0d353ce08f7be0","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"margin":{"doc_sha":"ab774a7da8cfe9bf","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"move":{"doc_sha":"5c74f429970a2d2b","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"move(dx, dy) or (delta) -> None"},"name":{"doc_sha":"0cc0684f26d8a660","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"on_click":{"doc_sha":"3a340282000297e0","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"on_enter":{"doc_sha":"48c996802e262396","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"on_exit":{"doc_sha":"c14ef0d6d6d40b0e","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"on_move":{"doc_sha":"9c3e26818b5b25b0","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"opacity":{"doc_sha":"c1a7ef41e074519e","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"origin":{"doc_sha":"270bdf366c74bb1d","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"outline":{"doc_sha":"20e9fd42a7ba4bb8","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"outline_color":{"doc_sha":"3fbed883fee308fe","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"parent":{"doc_sha":"171b0de4e295c55b","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"pos":{"doc_sha":"5f9d88ff075d2cfc","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"radius":{"doc_sha":"00a41207192c55a8","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"realign":{"doc_sha":"8dfa7333a49aa697","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"realign() -> None"},"resize":{"doc_sha":"005a04f1d129ebe5","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"resize(width, height) or (size) -> None"},"rotate_with_camera":{"doc_sha":"5c3af880218d3a08","kind":"property","lifecycle":{"modified":{"commit":"4bb1966","version":"0.2.8-dev"},"since":"0.2.8"},"signature":""},"rotation":{"doc_sha":"61ba3e66a0841a08","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"vert_margin":{"doc_sha":"e2042be923bc6fcb","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"visible":{"doc_sha":"49221a4ae830f3b8","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"z_index":{"doc_sha":"0cb3df5278d2658b","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""}}},"Color":{"doc_sha":"f1f664405900332f","kind":"class","lifecycle":{"since":"0.2.8"},"members":{"a":{"doc_sha":"ef61630ad34f8e2a","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"b":{"doc_sha":"2e2e91e93833fdd0","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"from_hex":{"doc_sha":"24d1ff553133bb7e","kind":"classmethod","lifecycle":{"since":"0.2.8"},"signature":"from_hex(hex_string: str) -> Color"},"g":{"doc_sha":"716ced374bcc2dc4","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"lerp":{"doc_sha":"290e204f3538e406","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"lerp(other: Color, t: float) -> Color"},"r":{"doc_sha":"ef64cae6296f68cb","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"to_hex":{"doc_sha":"d0cf65b284158877","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"to_hex() -> str"}}},"ColorLayer":{"doc_sha":"f8a87b6184c9a9b6","kind":"class","lifecycle":{"since":"0.2.8"},"members":{"apply_gradient":{"doc_sha":"20390111f0652ae0","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"apply_gradient(source: HeightMap, range: tuple, color_low: Color, color_high: Color) -> ColorLayer"},"apply_perspective":{"doc_sha":"a909e1b1c0373bc0","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"apply_perspective(entity: Entity, visible: Color | None = None, discovered: Color | None = None, unknown: Color | None = None) -> None"},"apply_ranges":{"doc_sha":"37fc5a26fda9cc0c","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"apply_ranges(source: HeightMap, ranges: list) -> ColorLayer"},"apply_threshold":{"doc_sha":"59cc9b72322cc677","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"apply_threshold(source: HeightMap, range: tuple, color: Color) -> ColorLayer"},"at":{"doc_sha":"fef47313dcef9247","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"at(pos: tuple | Vector) or (x: int, y: int) -> Color"},"clear_perspective":{"doc_sha":"fec237fe4cbda340","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"clear_perspective() -> None"},"draw_fov":{"doc_sha":"ff965b12c888d092","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"draw_fov(source: tuple, radius: int | None = None, fov: FOV | None = None, visible: Color | None = None, discovered: Color | None = None, unknown: Color | None = None) -> None"},"edit":{"doc_sha":"b0edb9c8f4c1fe76","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"edit() -> context manager"},"fill":{"doc_sha":"f6ff376fd71ae90b","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"fill(color: Color) -> None"},"fill_rect":{"doc_sha":"bc8b20da214aa37d","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"fill_rect(pos: tuple, size: tuple, color: Color) -> None"},"grid":{"doc_sha":"b965c55d57de78fa","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"grid_size":{"doc_sha":"eac88eb0f7b16cbf","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"name":{"doc_sha":"dd282cc9273e2e36","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"set":{"doc_sha":"36801cc04606ee0b","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"set(pos, color: Color) -> None"},"update_perspective":{"doc_sha":"c9caac507fbc4921","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"update_perspective() -> None"},"visible":{"doc_sha":"ecea81ec16e31c19","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"z_index":{"doc_sha":"eb13c1b4de11ce63","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""}}},"DijkstraMap":{"doc_sha":"822ccf5b086d37ec","kind":"class","lifecycle":{"since":"0.2.8"},"members":{"descent_step":{"doc_sha":"fd7b649f1f3eb586","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"descent_step(pos: Vector | tuple) -> Vector | None"},"distance":{"doc_sha":"92b40108bb47bf88","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"distance(pos: Vector | tuple) -> float | None"},"invert":{"doc_sha":"3e5ae145dafc2c1d","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"invert() -> DijkstraMap"},"path_from":{"doc_sha":"3dcf638bff837f2d","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"path_from(pos: Vector | tuple) -> AStarPath"},"root":{"doc_sha":"95824bac9c46ad83","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"step_from":{"doc_sha":"ac9bc93f671d3936","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"step_from(pos: Vector | tuple) -> Vector | None"},"to_heightmap":{"doc_sha":"0ba542e6509b510b","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"to_heightmap(size=None, unreachable=-1.0) -> HeightMap"}}},"DiscreteMap":{"doc_sha":"4d6d00ffa3ac62a9","kind":"class","lifecycle":{"since":"0.2.8"},"members":{"add":{"doc_sha":"1e5a5b83f555fa87","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"add(other: DiscreteMap | int, *, pos=None, source_pos=None, size=None) -> DiscreteMap"},"bitwise_and":{"doc_sha":"1644af83b8699792","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"bitwise_and(other: DiscreteMap, *, pos=None, source_pos=None, size=None) -> DiscreteMap"},"bitwise_or":{"doc_sha":"c8262af2fdccdeeb","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"bitwise_or(other: DiscreteMap, *, pos=None, source_pos=None, size=None) -> DiscreteMap"},"bitwise_xor":{"doc_sha":"01809471595bf8e7","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"bitwise_xor(other: DiscreteMap, *, pos=None, source_pos=None, size=None) -> DiscreteMap"},"bool":{"doc_sha":"a999f0f09edf7bf3","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"bool(condition: int | set | callable) -> DiscreteMap"},"clear":{"doc_sha":"b04d176e9c7bf9d2","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"clear() -> DiscreteMap"},"copy_from":{"doc_sha":"efea5a0aa9a8d9c9","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"copy_from(other: DiscreteMap, *, pos=None, source_pos=None, size=None) -> DiscreteMap"},"count":{"doc_sha":"5e3303ddc3a9e3ae","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"count(value: int) -> int"},"count_range":{"doc_sha":"ac4e0298919fd16e","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"count_range(min_val: int, max_val: int) -> int"},"enum_type":{"doc_sha":"f27f1ad46e2d5afe","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"fill":{"doc_sha":"7f358bdcc5c2c9f0","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"fill(value: int, *, pos=None, size=None) -> DiscreteMap"},"from_bytes":{"doc_sha":"b1c09c737d43ffa9","kind":"classmethod","lifecycle":{"since":"0.2.8"},"signature":"from_bytes(data: bytes, size: tuple[int, int], *, enum: type = None) -> DiscreteMap"},"from_heightmap":{"doc_sha":"0e89bb4262b19ea3","kind":"classmethod","lifecycle":{"since":"0.2.8"},"signature":"from_heightmap(hmap: HeightMap, mapping: list[tuple[tuple[float,float], int]], *, enum=None) -> DiscreteMap"},"get":{"doc_sha":"a1097b27b43d6a27","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"get(x, y) or (pos) -> int | Enum"},"histogram":{"doc_sha":"b2d9df4f14aedd51","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"histogram() -> dict[int, int]"},"invert":{"doc_sha":"0f52aecfe57ff499","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"invert() -> DiscreteMap"},"mask":{"doc_sha":"0d9628616f4592f4","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"mask() -> memoryview"},"max":{"doc_sha":"144265dd0da983a8","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"max(other: DiscreteMap, *, pos=None, source_pos=None, size=None) -> DiscreteMap"},"min":{"doc_sha":"ff90ceef60a45959","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"min(other: DiscreteMap, *, pos=None, source_pos=None, size=None) -> DiscreteMap"},"min_max":{"doc_sha":"22018caaf1bdb60e","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"min_max() -> tuple[int, int]"},"multiply":{"doc_sha":"430abcb2b647132f","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"multiply(factor: float, *, pos=None, size=None) -> DiscreteMap"},"set":{"doc_sha":"8439c14e5785f7e4","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"set(x: int, y: int, value: int) -> None"},"size":{"doc_sha":"fc6c37c9d3916c5d","kind":"property","lifecycle":{"since":"0.2.8"},"signature":"Dimensions (width, height) of the map. Read-only."},"subtract":{"doc_sha":"aeae8042663b9973","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"subtract(other: DiscreteMap | int, *, pos=None, source_pos=None, size=None) -> DiscreteMap"},"to_bytes":{"doc_sha":"4d7c4b5939a546bd","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"to_bytes() -> bytes"},"to_heightmap":{"doc_sha":"b0744db8f889b5d9","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"to_heightmap(mapping: dict[int, float] = None) -> HeightMap"}}},"Drawable":{"doc_sha":"bb28a8d677f24b33","kind":"class","lifecycle":{"since":"0.2.8"},"members":{"move":{"doc_sha":"4a8f9ca3692c6b58","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"move(dx, dy) or (delta) -> None"},"on_click":{"doc_sha":"b3b8de4e294d71b6","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"opacity":{"doc_sha":"c1a7ef41e074519e","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"resize":{"doc_sha":"980bdc9065fc077b","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"resize(width, height) or (size) -> None"},"visible":{"doc_sha":"49221a4ae830f3b8","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"z_index":{"doc_sha":"328d87ae44d9fb71","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""}}},"Easing":{"doc_sha":"e3b0c44298fc1c14","kind":"class","lifecycle":{"since":"0.2.8"},"members":{"as_integer_ratio":{"doc_sha":"3eb115fab0d0e0ec","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"($self, /)"},"bit_count":{"doc_sha":"f276da700b5a2e37","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"($self, /)"},"bit_length":{"doc_sha":"15b29db62fd0fa6c","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"($self, /)"},"conjugate":{"doc_sha":"6517a002faad7c75","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"($self, /)"},"denominator":{"doc_sha":"5051eb1570768851","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"from_bytes":{"doc_sha":"64672607865643a7","kind":"classmethod","lifecycle":{"since":"0.2.8"},"signature":"($type, /, bytes, byteorder='big', *, signed=False)"},"imag":{"doc_sha":"ef78ea410df39b26","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"is_integer":{"doc_sha":"6176763fbac2fd22","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"($self, /)"},"numerator":{"doc_sha":"ed255748ce2eda7c","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"real":{"doc_sha":"d44497f1990af6fe","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"to_bytes":{"doc_sha":"b452319e48e64764","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"($self, /, length=1, byteorder='big', *, signed=False)"}}},"Entity":{"doc_sha":"7231aaf2e3b107d0","kind":"class","lifecycle":{"since":"0.2.8"},"members":{"add_label":{"doc_sha":"be06d30bbd5a41ca","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"add_label(label: str) -> None"},"animate":{"doc_sha":"a06820cecef57a9a","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"animate(property: str, target: Any, duration: float, easing=None, delta=False, loop=False, callback=None, conflict_mode='replace') -> Animation"},"at":{"doc_sha":"b5775513388a1464","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"at(x: int, y: int) -> GridPoint | None"},"behavior_type":{"doc_sha":"745936b7029480fd","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"cell_pos":{"doc_sha":"f8feaba9f3652ca0","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"cell_x":{"doc_sha":"8bdc1199bfb15967","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"cell_y":{"doc_sha":"8fd78a55ede4b259","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"default_behavior":{"doc_sha":"f0a1ea99426408dd","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"die":{"doc_sha":"1369fecbc5a1862a","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"die() -> None"},"draw_pos":{"doc_sha":"2769bf67eec13406","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"find_path":{"doc_sha":"1a672b91a00eb597","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"find_path(target, diagonal_cost: float = 1.41, collide: str = None) -> AStarPath | None"},"grid":{"doc_sha":"d02c7b8c3beb5662","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"grid_pos":{"doc_sha":"d2e9f06a9d9f0a3c","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"grid_x":{"doc_sha":"4108b9ec31aff2c0","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"grid_y":{"doc_sha":"26f7190f34a208b2","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"has_label":{"doc_sha":"0e65dc586b3c8d18","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"has_label(label: str) -> bool"},"index":{"doc_sha":"74e24820bb380594","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"index() -> int"},"labels":{"doc_sha":"4e16ef3312d5b4e7","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"move":{"doc_sha":"5c74f429970a2d2b","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"move(dx, dy) or (delta) -> None"},"move_speed":{"doc_sha":"a3b3867a6d3b830a","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"name":{"doc_sha":"708f75f696069d69","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"opacity":{"doc_sha":"6caae59e941bebf1","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"path_to":{"doc_sha":"5b11e33c92caac2b","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"path_to(x: int, y: int) -> list"},"perspective_map":{"doc_sha":"f61894673d8c27a7","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"pos":{"doc_sha":"172ff9a95e621eaf","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"remove_label":{"doc_sha":"a4a541d7e9cf7275","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"remove_label(label: str) -> None"},"resize":{"doc_sha":"005a04f1d129ebe5","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"resize(width, height) or (size) -> None"},"set_behavior":{"doc_sha":"328826510c72f049","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"set_behavior(type, waypoints=None, turns: int = 0, path=None, pathfinder=None) -> None"},"shader":{"doc_sha":"04503c544358f827","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"sight_radius":{"doc_sha":"4a9ec6c3788b093a","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"sprite_grid":{"doc_sha":"a5ff2c3e5190bc41","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"sprite_index":{"doc_sha":"ca285ed1c6f460df","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"sprite_offset":{"doc_sha":"a92dae4a2555fa7b","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"sprite_offset_x":{"doc_sha":"7ba6504a9bbbf681","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"sprite_offset_y":{"doc_sha":"24d4546e182bb2c8","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"step":{"doc_sha":"8877861183e50020","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"target_label":{"doc_sha":"85acb11a7e713a41","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"texture":{"doc_sha":"1588a0d4779fd069","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"tile_height":{"doc_sha":"7f2ef7cb6d425730","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"tile_size":{"doc_sha":"1a647ababfde2daa","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"tile_width":{"doc_sha":"5d72ddf874e9db54","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"turn_order":{"doc_sha":"eba665bc9c7e75ee","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"uniforms":{"doc_sha":"d302457c5aaa495e","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"update_visibility":{"doc_sha":"d096cd998ed99b49","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"update_visibility() -> None"},"visible":{"doc_sha":"2bca3a265b44b270","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"visible_entities":{"doc_sha":"2482fbbecbfac7ea","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"visible_entities(fov=None, radius: int | None = None) -> list[Entity]"},"x":{"doc_sha":"4ba6a677ea35e7ba","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"y":{"doc_sha":"52b85f7dc8a7cb27","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""}}},"Entity3D":{"doc_sha":"0daee46cf6343e9c","kind":"class","lifecycle":{"since":"0.2.8"},"members":{"anim_clip":{"doc_sha":"f69294fc08fc1d6c","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"anim_frame":{"doc_sha":"41bdd9b6f4407d16","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"anim_loop":{"doc_sha":"b267a372c1aef2cd","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"anim_paused":{"doc_sha":"e100f0be03f09b09","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"anim_speed":{"doc_sha":"33c581c9533b5169","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"anim_time":{"doc_sha":"6b811c83dd2a2184","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"animate":{"doc_sha":"fcf44af080ce16a7","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"animate(property, target, duration, easing=None, delta=False, callback=None, conflict_mode=None)"},"at":{"doc_sha":"69d6101c79794f90","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"at(x, z) -> dict"},"auto_animate":{"doc_sha":"867ca7a9c2b5a4ab","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"clear_path":{"doc_sha":"989bb0a431b58e17","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"clear_path()"},"color":{"doc_sha":"ea9ab21659b6e392","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"follow_path":{"doc_sha":"f514b7092f561550","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"follow_path(path)"},"grid_pos":{"doc_sha":"f20411521f420193","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"idle_clip":{"doc_sha":"855c97611053652c","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"is_moving":{"doc_sha":"b31c1090228993e4","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"model":{"doc_sha":"26ae36cf867d07e9","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"name":{"doc_sha":"acea3a6577d65fbc","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"on_anim_complete":{"doc_sha":"5cc47894576d983d","kind":"property","lifecycle":{"since":"0.2.8"},"signature":"Callback(entity, clip_name) when non-looping animation ends."},"path_to":{"doc_sha":"d6fe3936cc8e2d84","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"path_to(x, z) or path_to(pos=(x, z)) -> list"},"pos":{"doc_sha":"2488492030e9df40","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"rotation":{"doc_sha":"5b603e8c652baf50","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"scale":{"doc_sha":"b06782214d92e3f7","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"teleport":{"doc_sha":"d53364830cdc737a","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"teleport(x, z) or teleport(pos=(x, z))"},"update_visibility":{"doc_sha":"61886fb70daedf6b","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"update_visibility()"},"viewport":{"doc_sha":"e9f9154f00b47f9a","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"visible":{"doc_sha":"bc1ae0f4c6b5cb11","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"walk_clip":{"doc_sha":"c95d11b4e1901f28","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"world_pos":{"doc_sha":"bcc6673be9243dd5","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""}}},"EntityCollection3D":{"doc_sha":"55864ac691527ca9","kind":"class","lifecycle":{"since":"0.2.8"},"members":{"append":{"doc_sha":"43922f93aff7d723","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"append(entity)"},"clear":{"doc_sha":"f44ea077b10186d7","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"clear()"},"extend":{"doc_sha":"4fd2e7e83437c7ae","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"extend(iterable)"},"find":{"doc_sha":"e4182a3a3317aaae","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"find(name) -> Entity3D or None"},"pop":{"doc_sha":"cbcc90e2a433f277","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"pop(index=-1) -> Entity3D"},"remove":{"doc_sha":"b2f8fb276b91ec1e","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"remove(entity)"}}},"EntityCollection3DIter":{"doc_sha":"d71ec48b224bfacc","kind":"class","lifecycle":{"since":"0.2.8"},"members":{}},"FOV":{"doc_sha":"e3b0c44298fc1c14","kind":"class","lifecycle":{"since":"0.2.8"},"members":{"as_integer_ratio":{"doc_sha":"3eb115fab0d0e0ec","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"($self, /)"},"bit_count":{"doc_sha":"f276da700b5a2e37","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"($self, /)"},"bit_length":{"doc_sha":"15b29db62fd0fa6c","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"($self, /)"},"conjugate":{"doc_sha":"6517a002faad7c75","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"($self, /)"},"denominator":{"doc_sha":"5051eb1570768851","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"from_bytes":{"doc_sha":"64672607865643a7","kind":"classmethod","lifecycle":{"since":"0.2.8"},"signature":"($type, /, bytes, byteorder='big', *, signed=False)"},"imag":{"doc_sha":"ef78ea410df39b26","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"is_integer":{"doc_sha":"6176763fbac2fd22","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"($self, /)"},"numerator":{"doc_sha":"ed255748ce2eda7c","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"real":{"doc_sha":"d44497f1990af6fe","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"to_bytes":{"doc_sha":"b452319e48e64764","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"($self, /, length=1, byteorder='big', *, signed=False)"}}},"Font":{"doc_sha":"245b4e9b0ca66fc9","kind":"class","lifecycle":{"since":"0.2.8"},"members":{"family":{"doc_sha":"0cf26d3038432764","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"source":{"doc_sha":"efc970576a584cde","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""}}},"Frame":{"doc_sha":"4d53c3cb9e9e4611","kind":"class","lifecycle":{"since":"0.2.8"},"members":{"align":{"doc_sha":"58657c3cfa010f31","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"animate":{"doc_sha":"400df4f5893261c5","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"animate(property: str, target: Any, duration: float, easing=None, delta=False, loop=False, callback=None, conflict_mode='replace') -> Animation"},"bounds":{"doc_sha":"d3186ac3b3ff8bed","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"cache_subtree":{"doc_sha":"0b8d1175192c1ce2","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"children":{"doc_sha":"b2cf42011b86727c","kind":"property","lifecycle":{"modified":{"commit":"4c0953c","version":"0.2.8-dev"},"since":"0.2.8"},"signature":""},"clip_children":{"doc_sha":"d12b2c4d1d2bb6cb","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"fill_color":{"doc_sha":"8cfbe5aa6dd6da27","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"global_bounds":{"doc_sha":"aa4de9a20df55c2b","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"global_position":{"doc_sha":"b8dabbecc9fccd2a","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"grid_pos":{"doc_sha":"2f5b05882c4ad39f","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"grid_size":{"doc_sha":"9c8707ad589e5d8a","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"h":{"doc_sha":"ced8010da828cc93","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"horiz_margin":{"doc_sha":"0ef227a4cae6e675","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"hovered":{"doc_sha":"af0d353ce08f7be0","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"margin":{"doc_sha":"ab774a7da8cfe9bf","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"move":{"doc_sha":"5c74f429970a2d2b","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"move(dx, dy) or (delta) -> None"},"name":{"doc_sha":"90a2ab0ec07c00eb","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"on_click":{"doc_sha":"b3b8de4e294d71b6","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"on_enter":{"doc_sha":"48c996802e262396","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"on_exit":{"doc_sha":"c14ef0d6d6d40b0e","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"on_move":{"doc_sha":"9c3e26818b5b25b0","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"opacity":{"doc_sha":"c1a7ef41e074519e","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"origin":{"doc_sha":"270bdf366c74bb1d","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"outline":{"doc_sha":"c07b6dec9da5b9ea","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"outline_color":{"doc_sha":"cd98fcaf39f0bc56","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"parent":{"doc_sha":"171b0de4e295c55b","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"pos":{"doc_sha":"55a118e7889fd555","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"realign":{"doc_sha":"8dfa7333a49aa697","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"realign() -> None"},"resize":{"doc_sha":"005a04f1d129ebe5","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"resize(width, height) or (size) -> None"},"rotate_with_camera":{"doc_sha":"5c3af880218d3a08","kind":"property","lifecycle":{"modified":{"commit":"4bb1966","version":"0.2.8-dev"},"since":"0.2.8"},"signature":""},"rotation":{"doc_sha":"61ba3e66a0841a08","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"shader":{"doc_sha":"418952064319ffb5","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"uniforms":{"doc_sha":"b1f2699da618d002","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"vert_margin":{"doc_sha":"e2042be923bc6fcb","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"visible":{"doc_sha":"49221a4ae830f3b8","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"w":{"doc_sha":"bd51299002a099c5","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"x":{"doc_sha":"4ed17c625f2289e3","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"y":{"doc_sha":"fabaa45fd789c7fd","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"z_index":{"doc_sha":"328d87ae44d9fb71","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""}}},"Grid":{"doc_sha":"f88e43e992d43a86","kind":"class","lifecycle":{"since":"0.2.8"},"members":{"align":{"doc_sha":"58657c3cfa010f31","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"animate":{"doc_sha":"400df4f5893261c5","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"animate(property: str, target: Any, duration: float, easing=None, delta=False, loop=False, callback=None, conflict_mode='replace') -> Animation"},"bounds":{"doc_sha":"d3186ac3b3ff8bed","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"camera_rotation":{"doc_sha":"bc2cd502f52817e9","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"center":{"doc_sha":"913ef5c36eaf9635","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"center_camera":{"doc_sha":"00d37ccdedcd3a2e","kind":"method","lifecycle":{"since":"0.2.8-dev"},"signature":"center_camera(pos: tuple = None) -> None"},"center_x":{"doc_sha":"fbb0ff08116f3c80","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"center_y":{"doc_sha":"1bda27633025fb12","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"children":{"doc_sha":"9566d92d5619ee21","kind":"property","lifecycle":{"since":"0.2.8-dev"},"signature":""},"fill_color":{"doc_sha":"f26fb5da3de1789a","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"global_bounds":{"doc_sha":"aa4de9a20df55c2b","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"global_position":{"doc_sha":"b8dabbecc9fccd2a","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"grid_data":{"doc_sha":"fdf0084b8456fd45","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"h":{"doc_sha":"bd5a06618a50efad","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"horiz_margin":{"doc_sha":"0ef227a4cae6e675","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"hovered":{"doc_sha":"af0d353ce08f7be0","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"hovered_cell":{"doc_sha":"85c87c8aa013d0c4","kind":"property","lifecycle":{"since":"0.2.8-dev"},"signature":""},"margin":{"doc_sha":"ab774a7da8cfe9bf","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"move":{"doc_sha":"5c74f429970a2d2b","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"move(dx, dy) or (delta) -> None"},"name":{"doc_sha":"90a2ab0ec07c00eb","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"on_cell_click":{"doc_sha":"ada11ea920c71f29","kind":"property","lifecycle":{"since":"0.2.8-dev"},"signature":""},"on_cell_enter":{"doc_sha":"6ae7882d4f14f183","kind":"property","lifecycle":{"since":"0.2.8-dev"},"signature":""},"on_cell_exit":{"doc_sha":"729303b3a70b2a36","kind":"property","lifecycle":{"since":"0.2.8-dev"},"signature":""},"on_click":{"doc_sha":"4e33bd16b2dee20a","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"on_enter":{"doc_sha":"48c996802e262396","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"on_exit":{"doc_sha":"c14ef0d6d6d40b0e","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"on_move":{"doc_sha":"9c3e26818b5b25b0","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"opacity":{"doc_sha":"c1a7ef41e074519e","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"origin":{"doc_sha":"270bdf366c74bb1d","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"parent":{"doc_sha":"171b0de4e295c55b","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"perspective":{"doc_sha":"8854eef3677c4423","kind":"property","lifecycle":{"since":"0.2.8-dev"},"signature":""},"perspective_enabled":{"doc_sha":"abd2223cd551e1ca","kind":"property","lifecycle":{"since":"0.2.8-dev"},"signature":""},"pos":{"doc_sha":"7f556820c51df2c4","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"realign":{"doc_sha":"8dfa7333a49aa697","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"realign() -> None"},"resize":{"doc_sha":"005a04f1d129ebe5","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"resize(width, height) or (size) -> None"},"rotate_with_camera":{"doc_sha":"5c3af880218d3a08","kind":"property","lifecycle":{"modified":{"commit":"4bb1966","version":"0.2.8-dev"},"since":"0.2.8"},"signature":""},"rotation":{"doc_sha":"61ba3e66a0841a08","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"shader":{"doc_sha":"418952064319ffb5","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"size":{"doc_sha":"cfff2bccd482c2ef","kind":"property","lifecycle":{"since":"0.2.8-dev"},"signature":""},"texture":{"doc_sha":"b9bfb54e5571bfc9","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"uniforms":{"doc_sha":"b1f2699da618d002","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"vert_margin":{"doc_sha":"e2042be923bc6fcb","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"visible":{"doc_sha":"49221a4ae830f3b8","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"w":{"doc_sha":"ec55a5a578c4d993","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"x":{"doc_sha":"2449b0f04153753c","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"y":{"doc_sha":"7958c0625706f2d2","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"z_index":{"doc_sha":"0cb3df5278d2658b","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"zoom":{"doc_sha":"e56c4b61e0303f09","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""}}},"GridData":{"doc_sha":"793d86e4e51839f6","kind":"class","lifecycle":{"since":"0.2.8-dev"},"members":{"add_layer":{"doc_sha":"ad48373d656e6a88","kind":"method","lifecycle":{"since":"0.2.8-dev"},"signature":"add_layer(layer: ColorLayer | TileLayer) -> ColorLayer | TileLayer"},"apply_ranges":{"doc_sha":"f74ca906e65a4bec","kind":"method","lifecycle":{"since":"0.2.8-dev"},"signature":"apply_ranges(source: HeightMap, ranges: list) -> GridData"},"apply_threshold":{"doc_sha":"823c3779c6610389","kind":"method","lifecycle":{"since":"0.2.8-dev"},"signature":"apply_threshold(source: HeightMap, range: tuple, walkable: bool = None, transparent: bool = None) -> GridData"},"at":{"doc_sha":"0f12b1f70eaec14b","kind":"method","lifecycle":{"since":"0.2.8-dev"},"signature":"at(x: int, y: int) -> GridPoint"},"clear_dijkstra_maps":{"doc_sha":"d470811b59885b93","kind":"method","lifecycle":{"since":"0.2.8-dev"},"signature":"clear_dijkstra_maps() -> None"},"compute_fov":{"doc_sha":"2522197d48d43d98","kind":"method","lifecycle":{"since":"0.2.8-dev"},"signature":"compute_fov(pos, radius: int = 0, light_walls: bool = True, algorithm: FOV | int = FOV.BASIC) -> None"},"entities":{"doc_sha":"5c361e2150645749","kind":"property","lifecycle":{"since":"0.2.8-dev"},"signature":""},"entities_in_radius":{"doc_sha":"820857d7995c8fe6","kind":"method","lifecycle":{"since":"0.2.8-dev"},"signature":"entities_in_radius(pos: tuple | Vector, radius: float) -> list"},"find_path":{"doc_sha":"3de3f1a19ba24df8","kind":"method","lifecycle":{"since":"0.2.8-dev"},"signature":"find_path(start, end, diagonal_cost: float = 1.41, collide: str = None, heuristic = None, weight: float = 1.0) -> AStarPath | None"},"fov":{"doc_sha":"bb929647d575d246","kind":"property","lifecycle":{"since":"0.2.8-dev"},"signature":""},"fov_radius":{"doc_sha":"bf76a3199e6d7f0b","kind":"property","lifecycle":{"since":"0.2.8-dev"},"signature":""},"get_dijkstra_map":{"doc_sha":"af44def04f5c942e","kind":"method","lifecycle":{"since":"0.2.8-dev"},"signature":"get_dijkstra_map(root=None, diagonal_cost: float = 1.41, collide: str = None, roots=None) -> DijkstraMap"},"grid_h":{"doc_sha":"f96615d57a28b090","kind":"property","lifecycle":{"since":"0.2.8-dev"},"signature":""},"grid_size":{"doc_sha":"f1aff956f432c1fe","kind":"property","lifecycle":{"since":"0.2.8-dev"},"signature":""},"grid_w":{"doc_sha":"e2f7f55bad731ce6","kind":"property","lifecycle":{"since":"0.2.8-dev"},"signature":""},"is_in_fov":{"doc_sha":"3f871a007bbff04c","kind":"method","lifecycle":{"since":"0.2.8-dev"},"signature":"is_in_fov(x: int, y: int) -> bool"},"layer":{"doc_sha":"38d9c25b2b0b172b","kind":"method","lifecycle":{"since":"0.2.8-dev"},"signature":"layer(name: str) -> ColorLayer | TileLayer | None"},"layers":{"doc_sha":"9d6207bfc4909360","kind":"property","lifecycle":{"since":"0.2.8-dev"},"signature":""},"remove_layer":{"doc_sha":"7876426544d2ec0d","kind":"method","lifecycle":{"since":"0.2.8-dev"},"signature":"remove_layer(name_or_layer: str | ColorLayer | TileLayer) -> None"},"step":{"doc_sha":"38146d2fc84d70f3","kind":"method","lifecycle":{"since":"0.2.8-dev"},"signature":"step(n: int = 1, turn_order: int = None) -> None"},"texture":{"doc_sha":"a2911dd21b916bc0","kind":"property","lifecycle":{"since":"0.2.8-dev"},"signature":""}}},"GridView":{"doc_sha":"f88e43e992d43a86","kind":"class","lifecycle":{"since":"0.2.8"},"members":{"align":{"doc_sha":"58657c3cfa010f31","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"animate":{"doc_sha":"400df4f5893261c5","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"animate(property: str, target: Any, duration: float, easing=None, delta=False, loop=False, callback=None, conflict_mode='replace') -> Animation"},"bounds":{"doc_sha":"d3186ac3b3ff8bed","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"camera_rotation":{"doc_sha":"bc2cd502f52817e9","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"center":{"doc_sha":"913ef5c36eaf9635","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"center_camera":{"doc_sha":"00d37ccdedcd3a2e","kind":"method","lifecycle":{"since":"0.2.8-dev"},"signature":"center_camera(pos: tuple = None) -> None"},"center_x":{"doc_sha":"fbb0ff08116f3c80","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"center_y":{"doc_sha":"1bda27633025fb12","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"children":{"doc_sha":"9566d92d5619ee21","kind":"property","lifecycle":{"since":"0.2.8-dev"},"signature":""},"fill_color":{"doc_sha":"f26fb5da3de1789a","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"global_bounds":{"doc_sha":"aa4de9a20df55c2b","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"global_position":{"doc_sha":"b8dabbecc9fccd2a","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"grid_data":{"doc_sha":"fdf0084b8456fd45","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"h":{"doc_sha":"bd5a06618a50efad","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"horiz_margin":{"doc_sha":"0ef227a4cae6e675","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"hovered":{"doc_sha":"af0d353ce08f7be0","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"hovered_cell":{"doc_sha":"85c87c8aa013d0c4","kind":"property","lifecycle":{"since":"0.2.8-dev"},"signature":""},"margin":{"doc_sha":"ab774a7da8cfe9bf","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"move":{"doc_sha":"5c74f429970a2d2b","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"move(dx, dy) or (delta) -> None"},"name":{"doc_sha":"90a2ab0ec07c00eb","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"on_cell_click":{"doc_sha":"ada11ea920c71f29","kind":"property","lifecycle":{"since":"0.2.8-dev"},"signature":""},"on_cell_enter":{"doc_sha":"6ae7882d4f14f183","kind":"property","lifecycle":{"since":"0.2.8-dev"},"signature":""},"on_cell_exit":{"doc_sha":"729303b3a70b2a36","kind":"property","lifecycle":{"since":"0.2.8-dev"},"signature":""},"on_click":{"doc_sha":"4e33bd16b2dee20a","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"on_enter":{"doc_sha":"48c996802e262396","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"on_exit":{"doc_sha":"c14ef0d6d6d40b0e","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"on_move":{"doc_sha":"9c3e26818b5b25b0","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"opacity":{"doc_sha":"c1a7ef41e074519e","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"origin":{"doc_sha":"270bdf366c74bb1d","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"parent":{"doc_sha":"171b0de4e295c55b","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"perspective":{"doc_sha":"8854eef3677c4423","kind":"property","lifecycle":{"since":"0.2.8-dev"},"signature":""},"perspective_enabled":{"doc_sha":"abd2223cd551e1ca","kind":"property","lifecycle":{"since":"0.2.8-dev"},"signature":""},"pos":{"doc_sha":"7f556820c51df2c4","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"realign":{"doc_sha":"8dfa7333a49aa697","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"realign() -> None"},"resize":{"doc_sha":"005a04f1d129ebe5","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"resize(width, height) or (size) -> None"},"rotate_with_camera":{"doc_sha":"5c3af880218d3a08","kind":"property","lifecycle":{"modified":{"commit":"4bb1966","version":"0.2.8-dev"},"since":"0.2.8"},"signature":""},"rotation":{"doc_sha":"61ba3e66a0841a08","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"shader":{"doc_sha":"418952064319ffb5","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"size":{"doc_sha":"cfff2bccd482c2ef","kind":"property","lifecycle":{"since":"0.2.8-dev"},"signature":""},"texture":{"doc_sha":"b9bfb54e5571bfc9","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"uniforms":{"doc_sha":"b1f2699da618d002","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"vert_margin":{"doc_sha":"e2042be923bc6fcb","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"visible":{"doc_sha":"49221a4ae830f3b8","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"w":{"doc_sha":"ec55a5a578c4d993","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"x":{"doc_sha":"2449b0f04153753c","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"y":{"doc_sha":"7958c0625706f2d2","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"z_index":{"doc_sha":"0cb3df5278d2658b","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"zoom":{"doc_sha":"e56c4b61e0303f09","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""}}},"HeightMap":{"doc_sha":"e5f86ba9c89f2a5a","kind":"class","lifecycle":{"since":"0.2.8"},"members":{"add":{"doc_sha":"d420dd760d7228af","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"add(other: HeightMap, *, pos=None, source_pos=None, size=None) -> HeightMap"},"add_bsp":{"doc_sha":"b70f06e432c91a7a","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"add_bsp(bsp: BSP, *, pos=None, select: str = 'leaves', nodes: list = None, shrink: int = 0, value: float = 1.0) -> HeightMap"},"add_constant":{"doc_sha":"f382eb3f3e667cb4","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"add_constant(value: float, *, pos=None, size=None) -> HeightMap"},"add_hill":{"doc_sha":"1ad2406960701682","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"add_hill(center, radius: float, height: float) -> HeightMap"},"add_noise":{"doc_sha":"3c3c15026ff43959","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"add_noise(source: NoiseSource, world_origin: tuple = (0.0, 0.0), world_size: tuple = None, mode: str = 'fbm', octaves: int = 4, scale: float = 1.0) -> HeightMap"},"add_voronoi":{"doc_sha":"b689af51f6867b78","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"add_voronoi(num_points: int, coefficients: tuple = (1.0, -0.5), seed: int = None) -> HeightMap"},"clamp":{"doc_sha":"60c08680fb281402","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"clamp(min: float = 0.0, max: float = 1.0, *, pos=None, size=None) -> HeightMap"},"clear":{"doc_sha":"2297d98f231b6b7b","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"clear() -> HeightMap"},"copy_from":{"doc_sha":"80d64f3f6e76f8a7","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"copy_from(other: HeightMap, *, pos=None, source_pos=None, size=None) -> HeightMap"},"count_in_range":{"doc_sha":"7af3d60f28d614b6","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"count_in_range(range: tuple[float, float]) -> int"},"dig_bezier":{"doc_sha":"f3327d6ddc165496","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"dig_bezier(points: tuple, start_radius: float, end_radius: float, start_height: float, end_height: float) -> HeightMap"},"dig_hill":{"doc_sha":"be55e494c4e50c3a","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"dig_hill(center, radius: float, target_height: float) -> HeightMap"},"fill":{"doc_sha":"4e998a0b1d85c4f2","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"fill(value: float, *, pos=None, size=None) -> HeightMap"},"get":{"doc_sha":"f55fcc655d43dc50","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"get(x, y) or (pos) -> float"},"get_interpolated":{"doc_sha":"5c3e0267d9cbeda2","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"get_interpolated(x, y) or (pos) -> float"},"get_normal":{"doc_sha":"6d4212add4e533f2","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"get_normal(x, y, water_level=0.0) or (pos, water_level=0.0) -> tuple[float, float, float]"},"get_slope":{"doc_sha":"4876967f738c7d67","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"get_slope(x, y) or (pos) -> float"},"inverse":{"doc_sha":"f6c4de6cd6a9ca0f","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"inverse() -> HeightMap"},"lerp":{"doc_sha":"967cacc180df98bc","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"lerp(other: HeightMap, t: float, *, pos=None, source_pos=None, size=None) -> HeightMap"},"max":{"doc_sha":"141515f8b1f23390","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"max(other: HeightMap, *, pos=None, source_pos=None, size=None) -> HeightMap"},"mid_point_displacement":{"doc_sha":"bfcbfe796d73410f","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"mid_point_displacement(roughness: float = 0.5, seed: int = None) -> HeightMap"},"min":{"doc_sha":"c51e934ea5047d91","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"min(other: HeightMap, *, pos=None, source_pos=None, size=None) -> HeightMap"},"min_max":{"doc_sha":"4e0b3cb7472a7cfe","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"min_max() -> tuple[float, float]"},"multiply":{"doc_sha":"83d2fcea22463d18","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"multiply(other: HeightMap, *, pos=None, source_pos=None, size=None) -> HeightMap"},"multiply_bsp":{"doc_sha":"21f401c651344592","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"multiply_bsp(bsp: BSP, *, pos=None, select: str = 'leaves', nodes: list = None, shrink: int = 0, value: float = 1.0) -> HeightMap"},"multiply_noise":{"doc_sha":"177d1d06a401af54","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"multiply_noise(source: NoiseSource, world_origin: tuple = (0.0, 0.0), world_size: tuple = None, mode: str = 'fbm', octaves: int = 4, scale: float = 1.0) -> HeightMap"},"normalize":{"doc_sha":"93ce05bf80d158ed","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"normalize(min: float = 0.0, max: float = 1.0, *, pos=None, size=None) -> HeightMap"},"rain_erosion":{"doc_sha":"4dbe6bd42d9f6e5b","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"rain_erosion(drops: int, erosion: float = 0.1, sedimentation: float = 0.05, seed: int = None) -> HeightMap"},"scale":{"doc_sha":"c85d9324b84c43ff","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"scale(factor: float, *, pos=None, size=None) -> HeightMap"},"size":{"doc_sha":"31846e24f4028b35","kind":"property","lifecycle":{"since":"0.2.8"},"signature":"Dimensions (width, height) of the heightmap. Read-only."},"smooth":{"doc_sha":"243be30aa73da43b","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"smooth(iterations: int = 1) -> HeightMap"},"sparse_kernel":{"doc_sha":"e3b28d2c3913ac74","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"sparse_kernel(weights: dict[tuple[int, int], float]) -> HeightMap"},"sparse_kernel_from":{"doc_sha":"ebeed84050ee0b2f","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"sparse_kernel_from(source: HeightMap, weights: dict[tuple[int, int], float]) -> None"},"subtract":{"doc_sha":"8e209f74aaad794d","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"subtract(other: HeightMap, *, pos=None, source_pos=None, size=None) -> HeightMap"},"threshold":{"doc_sha":"946f6a1fb850decb","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"threshold(range: tuple[float, float]) -> HeightMap"},"threshold_binary":{"doc_sha":"6ebe7bd7cbc112c2","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"threshold_binary(range: tuple[float, float], value: float = 1.0) -> HeightMap"}}},"Heuristic":{"doc_sha":"7582db951ab67fc5","kind":"class","lifecycle":{"since":"0.2.8"},"members":{"as_integer_ratio":{"doc_sha":"3eb115fab0d0e0ec","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"($self, /)"},"bit_count":{"doc_sha":"f276da700b5a2e37","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"($self, /)"},"bit_length":{"doc_sha":"15b29db62fd0fa6c","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"($self, /)"},"conjugate":{"doc_sha":"6517a002faad7c75","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"($self, /)"},"denominator":{"doc_sha":"5051eb1570768851","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"from_bytes":{"doc_sha":"64672607865643a7","kind":"classmethod","lifecycle":{"since":"0.2.8"},"signature":"($type, /, bytes, byteorder='big', *, signed=False)"},"imag":{"doc_sha":"ef78ea410df39b26","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"is_integer":{"doc_sha":"6176763fbac2fd22","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"($self, /)"},"numerator":{"doc_sha":"ed255748ce2eda7c","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"real":{"doc_sha":"d44497f1990af6fe","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"to_bytes":{"doc_sha":"b452319e48e64764","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"($self, /, length=1, byteorder='big', *, signed=False)"}}},"InputState":{"doc_sha":"c7b7558edda29fea","kind":"class","lifecycle":{"since":"0.2.8"},"members":{"as_integer_ratio":{"doc_sha":"3eb115fab0d0e0ec","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"($self, /)"},"bit_count":{"doc_sha":"f276da700b5a2e37","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"($self, /)"},"bit_length":{"doc_sha":"15b29db62fd0fa6c","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"($self, /)"},"conjugate":{"doc_sha":"6517a002faad7c75","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"($self, /)"},"denominator":{"doc_sha":"5051eb1570768851","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"from_bytes":{"doc_sha":"64672607865643a7","kind":"classmethod","lifecycle":{"since":"0.2.8"},"signature":"($type, /, bytes, byteorder='big', *, signed=False)"},"imag":{"doc_sha":"ef78ea410df39b26","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"is_integer":{"doc_sha":"6176763fbac2fd22","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"($self, /)"},"numerator":{"doc_sha":"ed255748ce2eda7c","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"real":{"doc_sha":"d44497f1990af6fe","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"to_bytes":{"doc_sha":"b452319e48e64764","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"($self, /, length=1, byteorder='big', *, signed=False)"}}},"Key":{"doc_sha":"bc4feb60367e4be7","kind":"class","lifecycle":{"since":"0.2.8"},"members":{"as_integer_ratio":{"doc_sha":"3eb115fab0d0e0ec","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"($self, /)"},"bit_count":{"doc_sha":"f276da700b5a2e37","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"($self, /)"},"bit_length":{"doc_sha":"15b29db62fd0fa6c","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"($self, /)"},"conjugate":{"doc_sha":"6517a002faad7c75","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"($self, /)"},"denominator":{"doc_sha":"5051eb1570768851","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"from_bytes":{"doc_sha":"64672607865643a7","kind":"classmethod","lifecycle":{"since":"0.2.8"},"signature":"($type, /, bytes, byteorder='big', *, signed=False)"},"imag":{"doc_sha":"ef78ea410df39b26","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"is_integer":{"doc_sha":"6176763fbac2fd22","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"($self, /)"},"numerator":{"doc_sha":"ed255748ce2eda7c","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"real":{"doc_sha":"d44497f1990af6fe","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"to_bytes":{"doc_sha":"b452319e48e64764","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"($self, /, length=1, byteorder='big', *, signed=False)"}}},"Keyboard":{"doc_sha":"afbde6bcf7bfe1c6","kind":"class","lifecycle":{"since":"0.2.8"},"members":{"alt":{"doc_sha":"051cb7dbc470d328","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"ctrl":{"doc_sha":"c062912b0b816bd2","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"shift":{"doc_sha":"614beb0d432c2ec0","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"system":{"doc_sha":"2292efb47472f5e7","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""}}},"LdtkProject":{"doc_sha":"d80a5fdda811cb33","kind":"class","lifecycle":{"since":"0.2.8"},"members":{"enums":{"doc_sha":"5697f9e75980e2be","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"level":{"doc_sha":"33b80035be788d03","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"level(name: str) -> dict"},"level_names":{"doc_sha":"9479f6c2bd85608d","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"ruleset":{"doc_sha":"ce72d154e5dd391c","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"ruleset(name: str) -> AutoRuleSet"},"ruleset_names":{"doc_sha":"28983e9702e0dc81","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"tileset":{"doc_sha":"30eabe3003acb244","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"tileset(name: str) -> TileSetFile"},"tileset_names":{"doc_sha":"77a45b51f49743ef","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"version":{"doc_sha":"e6d3d49ad9edfad8","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""}}},"Line":{"doc_sha":"8d778faa93b5d0d5","kind":"class","lifecycle":{"since":"0.2.8"},"members":{"align":{"doc_sha":"58657c3cfa010f31","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"animate":{"doc_sha":"400df4f5893261c5","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"animate(property: str, target: Any, duration: float, easing=None, delta=False, loop=False, callback=None, conflict_mode='replace') -> Animation"},"bounds":{"doc_sha":"d3186ac3b3ff8bed","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"color":{"doc_sha":"f7aca0b82eaae97e","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"end":{"doc_sha":"cc14ff1baa1b61fa","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"global_bounds":{"doc_sha":"aa4de9a20df55c2b","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"global_position":{"doc_sha":"b8dabbecc9fccd2a","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"grid_pos":{"doc_sha":"9df77e9a996208ba","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"grid_size":{"doc_sha":"e32208817299dfe9","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"horiz_margin":{"doc_sha":"0ef227a4cae6e675","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"hovered":{"doc_sha":"af0d353ce08f7be0","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"margin":{"doc_sha":"ab774a7da8cfe9bf","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"move":{"doc_sha":"5c74f429970a2d2b","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"move(dx, dy) or (delta) -> None"},"name":{"doc_sha":"feeb7cb507a4692d","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"on_click":{"doc_sha":"49cecfb458b010a1","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"on_enter":{"doc_sha":"48c996802e262396","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"on_exit":{"doc_sha":"c14ef0d6d6d40b0e","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"on_move":{"doc_sha":"9c3e26818b5b25b0","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"opacity":{"doc_sha":"c1a7ef41e074519e","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"origin":{"doc_sha":"270bdf366c74bb1d","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"parent":{"doc_sha":"171b0de4e295c55b","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"pos":{"doc_sha":"733a48ea3a730e60","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"realign":{"doc_sha":"8dfa7333a49aa697","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"realign() -> None"},"resize":{"doc_sha":"005a04f1d129ebe5","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"resize(width, height) or (size) -> None"},"rotate_with_camera":{"doc_sha":"5c3af880218d3a08","kind":"property","lifecycle":{"modified":{"commit":"4bb1966","version":"0.2.8-dev"},"since":"0.2.8"},"signature":""},"rotation":{"doc_sha":"61ba3e66a0841a08","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"start":{"doc_sha":"817049419558ea17","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"thickness":{"doc_sha":"b6c7f3e979028709","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"vert_margin":{"doc_sha":"e2042be923bc6fcb","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"visible":{"doc_sha":"49221a4ae830f3b8","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"z_index":{"doc_sha":"cecb3704206d35f7","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""}}},"Model3D":{"doc_sha":"be7fb5003ed48d84","kind":"class","lifecycle":{"since":"0.2.8"},"members":{"animation_clips":{"doc_sha":"0d15ad9839919fe5","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"bone_count":{"doc_sha":"6e389fb2496946bf","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"bounds":{"doc_sha":"4ddfb8f6ae5a8928","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"cube":{"doc_sha":"b49160efa8b0f71e","kind":"classmethod","lifecycle":{"since":"0.2.8"},"signature":"cube(size=1.0) -> Model3D"},"has_skeleton":{"doc_sha":"4e9ac073dac101b6","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"mesh_count":{"doc_sha":"49f8fe0ece668d77","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"name":{"doc_sha":"b577d22f9a18de0c","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"plane":{"doc_sha":"5e31b94e295b4429","kind":"classmethod","lifecycle":{"since":"0.2.8"},"signature":"plane(width=1.0, depth=1.0, segments=1) -> Model3D"},"sphere":{"doc_sha":"b385d8aaf0efd485","kind":"classmethod","lifecycle":{"since":"0.2.8"},"signature":"sphere(radius=0.5, segments=16, rings=12) -> Model3D"},"triangle_count":{"doc_sha":"b67c98fac8ffc8dd","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"vertex_count":{"doc_sha":"8b76e70b26a820df","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""}}},"Mouse":{"doc_sha":"e64f561c8a08bdec","kind":"class","lifecycle":{"since":"0.2.8"},"members":{"grabbed":{"doc_sha":"3da420bb9cac434f","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"left":{"doc_sha":"a4c7b98babc093cb","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"middle":{"doc_sha":"1306b1240c6f64e3","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"pos":{"doc_sha":"2ecba0a68d64976a","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"right":{"doc_sha":"08d5534c1c034b39","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"visible":{"doc_sha":"834527eb5579f3e3","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"x":{"doc_sha":"c142f98ee94bf3f7","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"y":{"doc_sha":"5095db9e9e163e92","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""}}},"MouseButton":{"doc_sha":"a5a7584949d18f01","kind":"class","lifecycle":{"since":"0.2.8"},"members":{"as_integer_ratio":{"doc_sha":"3eb115fab0d0e0ec","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"($self, /)"},"bit_count":{"doc_sha":"f276da700b5a2e37","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"($self, /)"},"bit_length":{"doc_sha":"15b29db62fd0fa6c","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"($self, /)"},"conjugate":{"doc_sha":"6517a002faad7c75","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"($self, /)"},"denominator":{"doc_sha":"5051eb1570768851","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"from_bytes":{"doc_sha":"64672607865643a7","kind":"classmethod","lifecycle":{"since":"0.2.8"},"signature":"($type, /, bytes, byteorder='big', *, signed=False)"},"imag":{"doc_sha":"ef78ea410df39b26","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"is_integer":{"doc_sha":"6176763fbac2fd22","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"($self, /)"},"numerator":{"doc_sha":"ed255748ce2eda7c","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"real":{"doc_sha":"d44497f1990af6fe","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"to_bytes":{"doc_sha":"b452319e48e64764","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"($self, /, length=1, byteorder='big', *, signed=False)"}}},"Music":{"doc_sha":"e13e6ad378ec0e91","kind":"class","lifecycle":{"since":"0.2.8"},"members":{"duration":{"doc_sha":"25607c5fa852d501","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"loop":{"doc_sha":"a165a91f8ff771f5","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"pause":{"doc_sha":"4eb6d242151147dc","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"pause() -> None"},"play":{"doc_sha":"f724039e0cdb2847","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"play() -> None"},"playing":{"doc_sha":"4451d007747fa3b1","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"position":{"doc_sha":"608a5f6d4ed927bc","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"source":{"doc_sha":"ee5d7818bae0a255","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"stop":{"doc_sha":"f3289bbb98a4da1f","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"stop() -> None"},"volume":{"doc_sha":"749fc9d89252cb0e","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""}}},"NoiseSource":{"doc_sha":"3bfa3b53984c85cb","kind":"class","lifecycle":{"since":"0.2.8"},"members":{"algorithm":{"doc_sha":"ba813d6c022f15f0","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"dimensions":{"doc_sha":"f534d20928d8d67a","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"fbm":{"doc_sha":"d13c8670fc68b9df","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"fbm(pos: tuple[float, ...], octaves: int = 4) -> float"},"get":{"doc_sha":"3d7776fffd7a0a39","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"get(pos: tuple[float, ...]) -> float"},"hurst":{"doc_sha":"11682e6dc42f2417","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"lacunarity":{"doc_sha":"4d07f7079f3938de","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"sample":{"doc_sha":"63fd0e680000d0f0","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"sample(size: tuple[int, int], world_origin: tuple[float, float] = (0.0, 0.0), world_size: tuple[float, float] = None, mode: str = 'fbm', octaves: int = 4) -> HeightMap"},"seed":{"doc_sha":"17bc1e90a8ce6b23","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"turbulence":{"doc_sha":"800c841f913b3780","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"turbulence(pos: tuple[float, ...], octaves: int = 4) -> float"}}},"Perspective":{"doc_sha":"e1c8553ea2a32a40","kind":"class","lifecycle":{"since":"0.2.8"},"members":{"as_integer_ratio":{"doc_sha":"3eb115fab0d0e0ec","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"($self, /)"},"bit_count":{"doc_sha":"f276da700b5a2e37","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"($self, /)"},"bit_length":{"doc_sha":"15b29db62fd0fa6c","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"($self, /)"},"conjugate":{"doc_sha":"6517a002faad7c75","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"($self, /)"},"denominator":{"doc_sha":"5051eb1570768851","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"from_bytes":{"doc_sha":"64672607865643a7","kind":"classmethod","lifecycle":{"since":"0.2.8"},"signature":"($type, /, bytes, byteorder='big', *, signed=False)"},"imag":{"doc_sha":"ef78ea410df39b26","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"is_integer":{"doc_sha":"6176763fbac2fd22","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"($self, /)"},"numerator":{"doc_sha":"ed255748ce2eda7c","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"real":{"doc_sha":"d44497f1990af6fe","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"to_bytes":{"doc_sha":"b452319e48e64764","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"($self, /, length=1, byteorder='big', *, signed=False)"}}},"PropertyBinding":{"doc_sha":"a23eb43edd46bb66","kind":"class","lifecycle":{"since":"0.2.8"},"members":{"is_valid":{"doc_sha":"40ada14485c362a7","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"property":{"doc_sha":"0dad06f4e661db2d","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"target":{"doc_sha":"3b15e1922fd910c9","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"value":{"doc_sha":"80d28b4e329837d8","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""}}},"Scene":{"doc_sha":"a156c42d77b20a3e","kind":"class","lifecycle":{"since":"0.2.8"},"members":{"activate":{"doc_sha":"54ae3cd3cf652660","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"activate(transition: Transition = None, duration: float = None) -> None"},"active":{"doc_sha":"a59f70d3c12a7cfe","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"children":{"doc_sha":"f21fb480dac856b6","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"name":{"doc_sha":"7feed093cdf9334a","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"on_key":{"doc_sha":"40ece618402a45f3","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"opacity":{"doc_sha":"9e60a4ae9bdb493b","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"pos":{"doc_sha":"50e29b0dd34a76d5","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"realign":{"doc_sha":"eef1e2dce9ee8894","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"realign() -> None"},"register":{"doc_sha":"7603337801a7a498","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"register() -> None"},"registered":{"doc_sha":"d9ff54f488ed5b87","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"unregister":{"doc_sha":"fda6be9891d1f502","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"unregister() -> None"},"visible":{"doc_sha":"a7f321f4013dfb9b","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""}}},"Shader":{"doc_sha":"aded56cb37cba9d8","kind":"class","lifecycle":{"since":"0.2.8"},"members":{"dynamic":{"doc_sha":"43d8bc587c3997e3","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"is_valid":{"doc_sha":"fcdf95eedfa8479e","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"set_uniform":{"doc_sha":"bdaf877e5bc28f21","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"set_uniform(name: str, value: float|tuple) -> None"},"source":{"doc_sha":"dbd462698aadf450","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""}}},"Sound":{"doc_sha":"8f31cca1781580d5","kind":"class","lifecycle":{"since":"0.2.8"},"members":{"buffer":{"doc_sha":"c79f4e433d4300ac","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"duration":{"doc_sha":"48daac900b6eaf36","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"loop":{"doc_sha":"a624dff0e72a5337","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"pause":{"doc_sha":"005627aca21692d3","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"pause() -> None"},"pitch":{"doc_sha":"80b38bfc202b4291","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"play":{"doc_sha":"ae090ee8c0320797","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"play() -> None"},"play_varied":{"doc_sha":"fd9b5cd6db3cb1cf","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"play_varied(pitch_range: float = 0.1, volume_range: float = 3.0) -> None"},"playing":{"doc_sha":"8a0c491fa38061d5","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"source":{"doc_sha":"1736ca7393d96c48","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"stop":{"doc_sha":"f3289bbb98a4da1f","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"stop() -> None"},"volume":{"doc_sha":"749fc9d89252cb0e","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""}}},"SoundBuffer":{"doc_sha":"426618cde68541be","kind":"class","lifecycle":{"since":"0.2.8"},"members":{"bit_crush":{"doc_sha":"66ae55272fd9d182","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"bit_crush(bits: int, rate_divisor: int) -> SoundBuffer"},"channels":{"doc_sha":"f3948c1ceb5d7aec","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"concat":{"doc_sha":"7ee10bfe76aae5e2","kind":"classmethod","lifecycle":{"since":"0.2.8"},"signature":"concat(buffers: list[SoundBuffer], overlap: float = 0.0) -> SoundBuffer"},"distortion":{"doc_sha":"a36442ca725e5cc6","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"distortion(drive: float) -> SoundBuffer"},"duration":{"doc_sha":"bc1c03f781e0372b","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"echo":{"doc_sha":"1b93f79c671068ab","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"echo(delay_ms: float, feedback: float, wet: float) -> SoundBuffer"},"from_samples":{"doc_sha":"556326f1f2cda4a7","kind":"classmethod","lifecycle":{"since":"0.2.8"},"signature":"from_samples(data: bytes, channels: int, sample_rate: int) -> SoundBuffer"},"gain":{"doc_sha":"22fc3aad26b084a5","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"gain(factor: float) -> SoundBuffer"},"high_pass":{"doc_sha":"7d8af12720a4bbae","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"high_pass(cutoff_hz: float) -> SoundBuffer"},"low_pass":{"doc_sha":"82cdc215e7da9310","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"low_pass(cutoff_hz: float) -> SoundBuffer"},"mix":{"doc_sha":"4df14cf741427da9","kind":"classmethod","lifecycle":{"since":"0.2.8"},"signature":"mix(buffers: list[SoundBuffer]) -> SoundBuffer"},"normalize":{"doc_sha":"5476b16208b1ac7b","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"normalize() -> SoundBuffer"},"pitch_shift":{"doc_sha":"ee9373f2d71fdaa8","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"pitch_shift(factor: float) -> SoundBuffer"},"reverb":{"doc_sha":"b6d0dcf1e5f75a42","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"reverb(room_size: float, damping: float, wet: float) -> SoundBuffer"},"reverse":{"doc_sha":"eb70bfa5da4c860d","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"reverse() -> SoundBuffer"},"sample_count":{"doc_sha":"5e96b7263eade6af","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"sample_rate":{"doc_sha":"a41417a71e10a2d6","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"sfxr":{"doc_sha":"5f45a4b7afc74066","kind":"classmethod","lifecycle":{"since":"0.2.8"},"signature":"sfxr(preset: str = None, seed: int = None, **params) -> SoundBuffer"},"sfxr_mutate":{"doc_sha":"68b5b653de4f55dc","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"sfxr_mutate(amount: float = 0.05, seed: int = None) -> SoundBuffer"},"sfxr_params":{"doc_sha":"7fd7a2356600014a","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"slice":{"doc_sha":"84099d0f46e6b7ad","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"slice(start: float, end: float) -> SoundBuffer"},"tone":{"doc_sha":"eaec1748a9089774","kind":"classmethod","lifecycle":{"since":"0.2.8"},"signature":"tone(frequency: float, duration: float, waveform: str = 'sine', ...) -> SoundBuffer"}}},"Sprite":{"doc_sha":"a687c4f393725428","kind":"class","lifecycle":{"since":"0.2.8"},"members":{"align":{"doc_sha":"58657c3cfa010f31","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"animate":{"doc_sha":"400df4f5893261c5","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"animate(property: str, target: Any, duration: float, easing=None, delta=False, loop=False, callback=None, conflict_mode='replace') -> Animation"},"bounds":{"doc_sha":"d3186ac3b3ff8bed","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"global_bounds":{"doc_sha":"aa4de9a20df55c2b","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"global_position":{"doc_sha":"b8dabbecc9fccd2a","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"grid_pos":{"doc_sha":"9df77e9a996208ba","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"grid_size":{"doc_sha":"e32208817299dfe9","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"horiz_margin":{"doc_sha":"0ef227a4cae6e675","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"hovered":{"doc_sha":"af0d353ce08f7be0","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"margin":{"doc_sha":"ab774a7da8cfe9bf","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"move":{"doc_sha":"5c74f429970a2d2b","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"move(dx, dy) or (delta) -> None"},"name":{"doc_sha":"90a2ab0ec07c00eb","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"on_click":{"doc_sha":"b3b8de4e294d71b6","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"on_enter":{"doc_sha":"48c996802e262396","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"on_exit":{"doc_sha":"c14ef0d6d6d40b0e","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"on_move":{"doc_sha":"9c3e26818b5b25b0","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"opacity":{"doc_sha":"c1a7ef41e074519e","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"origin":{"doc_sha":"270bdf366c74bb1d","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"parent":{"doc_sha":"171b0de4e295c55b","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"pos":{"doc_sha":"55a118e7889fd555","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"realign":{"doc_sha":"8dfa7333a49aa697","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"realign() -> None"},"resize":{"doc_sha":"005a04f1d129ebe5","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"resize(width, height) or (size) -> None"},"rotate_with_camera":{"doc_sha":"5c3af880218d3a08","kind":"property","lifecycle":{"modified":{"commit":"4bb1966","version":"0.2.8-dev"},"since":"0.2.8"},"signature":""},"rotation":{"doc_sha":"61ba3e66a0841a08","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"scale":{"doc_sha":"f91a66d322ee1a75","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"scale_x":{"doc_sha":"95f47a809a8ac927","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"scale_y":{"doc_sha":"8567691c22a9b5f8","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"shader":{"doc_sha":"418952064319ffb5","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"sprite_index":{"doc_sha":"826af9fc52ae5cef","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"texture":{"doc_sha":"df3e7671926aa0a3","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"uniforms":{"doc_sha":"b1f2699da618d002","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"vert_margin":{"doc_sha":"e2042be923bc6fcb","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"visible":{"doc_sha":"49221a4ae830f3b8","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"x":{"doc_sha":"528487f8deaaf789","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"y":{"doc_sha":"114ecec940477107","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"z_index":{"doc_sha":"328d87ae44d9fb71","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""}}},"Texture":{"doc_sha":"da368b536d9f844e","kind":"class","lifecycle":{"since":"0.2.8"},"members":{"composite":{"doc_sha":"0bf406582f618af3","kind":"classmethod","lifecycle":{"since":"0.2.8"},"signature":"composite(layers: list[Texture], sprite_width: int, sprite_height: int, name: str = '') -> Texture"},"display_height":{"doc_sha":"0dc1ccea45ff4e40","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"display_offset_x":{"doc_sha":"0ca738fa6559aabb","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"display_offset_y":{"doc_sha":"83b5bfadd26f4e79","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"display_width":{"doc_sha":"cfc6525a8a0b5bb7","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"from_bytes":{"doc_sha":"979f5a384770d87f","kind":"classmethod","lifecycle":{"since":"0.2.8"},"signature":"from_bytes(data: bytes, width: int, height: int, sprite_width: int, sprite_height: int, name: str = '') -> Texture"},"hsl_shift":{"doc_sha":"95f19de7a67646e3","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"hsl_shift(hue_shift: float, sat_shift: float = 0.0, lit_shift: float = 0.0) -> Texture"},"sheet_height":{"doc_sha":"59d4fb71cae72fba","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"sheet_width":{"doc_sha":"54ef54eb9c39a907","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"source":{"doc_sha":"51bee1cdb17257ba","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"sprite_count":{"doc_sha":"d1ed79d8b59eca99","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"sprite_height":{"doc_sha":"19b6a36f45f965e8","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"sprite_width":{"doc_sha":"6f9276522c3b2512","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""}}},"TileLayer":{"doc_sha":"5551e3bf9e668d89","kind":"class","lifecycle":{"since":"0.2.8"},"members":{"apply_ranges":{"doc_sha":"e443e268532fdd2f","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"apply_ranges(source: HeightMap, ranges: list) -> TileLayer"},"apply_threshold":{"doc_sha":"2253f4367656d073","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"apply_threshold(source: HeightMap, range: tuple, tile: int) -> TileLayer"},"at":{"doc_sha":"19975fea19b3fadb","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"at(pos: tuple | Vector) or (x: int, y: int) -> int"},"edit":{"doc_sha":"47c32fda0e88780a","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"edit() -> context manager"},"fill":{"doc_sha":"5c8eca820794a605","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"fill(index: int) -> None"},"fill_rect":{"doc_sha":"adfc5ff6d6e91cc8","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"fill_rect(pos: tuple, size: tuple, index: int) -> None"},"grid":{"doc_sha":"b965c55d57de78fa","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"grid_size":{"doc_sha":"eac88eb0f7b16cbf","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"name":{"doc_sha":"dd282cc9273e2e36","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"set":{"doc_sha":"2b242556ce84080f","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"set(pos, index: int) -> None"},"texture":{"doc_sha":"1d211a219f88c72c","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"visible":{"doc_sha":"ecea81ec16e31c19","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"z_index":{"doc_sha":"eb13c1b4de11ce63","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""}}},"TileMapFile":{"doc_sha":"b26cb3870e41a2c9","kind":"class","lifecycle":{"since":"0.2.8"},"members":{"apply_to_tile_layer":{"doc_sha":"436e4fa972bd79d0","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"apply_to_tile_layer(tile_layer: TileLayer, layer_name: str, tileset_index: int = 0) -> None"},"height":{"doc_sha":"72aa81aefdab3ddb","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"object_layer":{"doc_sha":"e658613bcbbe9b3a","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"object_layer(name: str) -> list[dict]"},"object_layer_names":{"doc_sha":"7af1cc1454cd97f9","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"orientation":{"doc_sha":"2ee759a156b5c903","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"properties":{"doc_sha":"f2cb52eb132891d8","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"resolve_gid":{"doc_sha":"4a72f1c9f61cec27","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"resolve_gid(gid: int) -> tuple[int, int]"},"tile_height":{"doc_sha":"cc47760c48b8c9be","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"tile_layer_data":{"doc_sha":"399fd655e6402442","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"tile_layer_data(name: str) -> list[int]"},"tile_layer_names":{"doc_sha":"e225e2d4b71155e7","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"tile_width":{"doc_sha":"c109808c1e2d3ca1","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"tileset":{"doc_sha":"a3e35f368c3cc89d","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"tileset(index: int) -> tuple[int, TileSetFile]"},"tileset_count":{"doc_sha":"e7347387a4bcfeae","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"width":{"doc_sha":"acef1b7138a85569","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""}}},"TileSetFile":{"doc_sha":"ec83598f02a06258","kind":"class","lifecycle":{"since":"0.2.8"},"members":{"columns":{"doc_sha":"5ba187696ce9596e","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"image_source":{"doc_sha":"f21faacc0392eec3","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"margin":{"doc_sha":"6e1b3cfd5b59af6a","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"name":{"doc_sha":"5e0a76b40f4db151","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"properties":{"doc_sha":"e0382abd62d297a6","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"spacing":{"doc_sha":"950416fc0b8f1837","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"tile_count":{"doc_sha":"df5ccd0141400827","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"tile_height":{"doc_sha":"71a327fd061c1cf2","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"tile_info":{"doc_sha":"b33077f44c79e050","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"tile_info(tile_id: int) -> dict | None"},"tile_width":{"doc_sha":"888787b1ef69f5d9","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"to_texture":{"doc_sha":"610adac758051370","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"to_texture() -> Texture"},"wang_set":{"doc_sha":"c3b15db4611c250a","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"wang_set(name: str) -> WangSet"},"wang_sets":{"doc_sha":"71541a1c5777b29f","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""}}},"Timer":{"doc_sha":"646feff7bdb817f5","kind":"class","lifecycle":{"since":"0.2.8"},"members":{"active":{"doc_sha":"0ac0f4a607819264","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"callback":{"doc_sha":"a943e7e99b75fa22","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"interval":{"doc_sha":"e32658ee97f94823","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"name":{"doc_sha":"98a62482b59640f6","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"once":{"doc_sha":"7d5a7d20bae3793e","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"pause":{"doc_sha":"bfe544cc3c9f1cda","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"pause() -> None"},"paused":{"doc_sha":"060df10cbfc34662","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"remaining":{"doc_sha":"e223a64ed6a1ec63","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"restart":{"doc_sha":"a8c305ae5017cce0","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"restart() -> None"},"resume":{"doc_sha":"66ab0cd39f579ef9","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"resume() -> None"},"start":{"doc_sha":"0b2b51d812995d34","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"start() -> None"},"stop":{"doc_sha":"9f69f5e68512116c","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"stop() -> None"},"stopped":{"doc_sha":"bffdb8aa4589c28b","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""}}},"Transition":{"doc_sha":"e3b0c44298fc1c14","kind":"class","lifecycle":{"since":"0.2.8"},"members":{"as_integer_ratio":{"doc_sha":"3eb115fab0d0e0ec","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"($self, /)"},"bit_count":{"doc_sha":"f276da700b5a2e37","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"($self, /)"},"bit_length":{"doc_sha":"15b29db62fd0fa6c","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"($self, /)"},"conjugate":{"doc_sha":"6517a002faad7c75","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"($self, /)"},"denominator":{"doc_sha":"5051eb1570768851","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"from_bytes":{"doc_sha":"64672607865643a7","kind":"classmethod","lifecycle":{"since":"0.2.8"},"signature":"($type, /, bytes, byteorder='big', *, signed=False)"},"imag":{"doc_sha":"ef78ea410df39b26","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"is_integer":{"doc_sha":"6176763fbac2fd22","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"($self, /)"},"numerator":{"doc_sha":"ed255748ce2eda7c","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"real":{"doc_sha":"d44497f1990af6fe","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"to_bytes":{"doc_sha":"b452319e48e64764","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"($self, /, length=1, byteorder='big', *, signed=False)"}}},"Traversal":{"doc_sha":"e3b0c44298fc1c14","kind":"class","lifecycle":{"since":"0.2.8"},"members":{"as_integer_ratio":{"doc_sha":"3eb115fab0d0e0ec","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"($self, /)"},"bit_count":{"doc_sha":"f276da700b5a2e37","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"($self, /)"},"bit_length":{"doc_sha":"15b29db62fd0fa6c","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"($self, /)"},"conjugate":{"doc_sha":"6517a002faad7c75","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"($self, /)"},"denominator":{"doc_sha":"5051eb1570768851","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"from_bytes":{"doc_sha":"64672607865643a7","kind":"classmethod","lifecycle":{"since":"0.2.8"},"signature":"($type, /, bytes, byteorder='big', *, signed=False)"},"imag":{"doc_sha":"ef78ea410df39b26","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"is_integer":{"doc_sha":"6176763fbac2fd22","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"($self, /)"},"numerator":{"doc_sha":"ed255748ce2eda7c","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"real":{"doc_sha":"d44497f1990af6fe","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"to_bytes":{"doc_sha":"b452319e48e64764","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"($self, /, length=1, byteorder='big', *, signed=False)"}}},"Trigger":{"doc_sha":"3f698ad8b187be9f","kind":"class","lifecycle":{"since":"0.2.8"},"members":{"as_integer_ratio":{"doc_sha":"3eb115fab0d0e0ec","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"($self, /)"},"bit_count":{"doc_sha":"f276da700b5a2e37","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"($self, /)"},"bit_length":{"doc_sha":"15b29db62fd0fa6c","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"($self, /)"},"conjugate":{"doc_sha":"6517a002faad7c75","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"($self, /)"},"denominator":{"doc_sha":"5051eb1570768851","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"from_bytes":{"doc_sha":"64672607865643a7","kind":"classmethod","lifecycle":{"since":"0.2.8"},"signature":"($type, /, bytes, byteorder='big', *, signed=False)"},"imag":{"doc_sha":"ef78ea410df39b26","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"is_integer":{"doc_sha":"6176763fbac2fd22","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"($self, /)"},"numerator":{"doc_sha":"ed255748ce2eda7c","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"real":{"doc_sha":"d44497f1990af6fe","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"to_bytes":{"doc_sha":"b452319e48e64764","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"($self, /, length=1, byteorder='big', *, signed=False)"}}},"Vector":{"doc_sha":"0c8ae9e2ef387065","kind":"class","lifecycle":{"since":"0.2.8"},"members":{"angle":{"doc_sha":"a0ca5f1f4a89d780","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"angle() -> float"},"copy":{"doc_sha":"120e723fd513e072","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"copy() -> Vector"},"distance_to":{"doc_sha":"4f5d26bbe8434f65","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"distance_to(other: Vector) -> float"},"dot":{"doc_sha":"d69442ea289adfb8","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"dot(other: Vector) -> float"},"floor":{"doc_sha":"c7e81a90c5420968","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"floor() -> Vector"},"int":{"doc_sha":"3d97cae0ad8a963c","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"magnitude":{"doc_sha":"75f61a474afe3121","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"magnitude() -> float"},"magnitude_squared":{"doc_sha":"c976db5b540f827b","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"magnitude_squared() -> float"},"normalize":{"doc_sha":"04b468da3fc60858","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"normalize() -> Vector"},"x":{"doc_sha":"0e322bdd857e652b","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"y":{"doc_sha":"bbac84f15b6740d9","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""}}},"Viewport3D":{"doc_sha":"c41e58effff86261","kind":"class","lifecycle":{"since":"0.2.8"},"members":{"add_billboard":{"doc_sha":"ae2357f78a17a437","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"add_billboard(billboard)"},"add_layer":{"doc_sha":"b4c0274f9217aaa1","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"add_layer(name, z_index=0) -> dict"},"add_mesh":{"doc_sha":"588dbdf9a48f17e5","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"add_mesh(layer_name, model, pos, rotation=0, scale=1.0) -> int"},"add_voxel_layer":{"doc_sha":"47f17324425647e1","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"add_voxel_layer(voxel_grid, z_index=0)"},"animate":{"doc_sha":"400df4f5893261c5","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"animate(property: str, target: Any, duration: float, easing=None, delta=False, loop=False, callback=None, conflict_mode='replace') -> Animation"},"apply_heightmap":{"doc_sha":"be8e92a25726238d","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"apply_heightmap(heightmap, y_scale=1.0)"},"apply_terrain_colors":{"doc_sha":"cf0e47c185484e70","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"apply_terrain_colors(layer_name, r_map, g_map, b_map)"},"apply_threshold":{"doc_sha":"3adda69e435613d7","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"apply_threshold(heightmap, min_height, max_height, walkable=True)"},"at":{"doc_sha":"7ec76d7602627cb9","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"at(x, z) -> VoxelPoint"},"bg_color":{"doc_sha":"c7e7141bd1cadaea","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"billboard_count":{"doc_sha":"4b432c5c560ee44f","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"billboard_count() -> int"},"bounds":{"doc_sha":"d3186ac3b3ff8bed","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"build_terrain":{"doc_sha":"5be2a61f78907326","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"build_terrain(layer_name, heightmap, y_scale=1.0, cell_size=1.0) -> int"},"camera_pos":{"doc_sha":"7bfead8afc2b8312","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"camera_target":{"doc_sha":"3ecf7bb4dbdf27e4","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"cell_size":{"doc_sha":"8c3fa657c8fdc2aa","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"clear_billboards":{"doc_sha":"63d6d64c39b46264","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"clear_billboards()"},"clear_meshes":{"doc_sha":"1d6eb71f274020cf","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"clear_meshes(layer_name)"},"clear_voxel_nav_region":{"doc_sha":"56901862e4dc595a","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"clear_voxel_nav_region(voxel_grid)"},"compute_fov":{"doc_sha":"93c68ed57619564e","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"compute_fov(origin, radius=10) -> list"},"enable_affine":{"doc_sha":"96ba76ed43593940","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"enable_dither":{"doc_sha":"da6733dd08751b72","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"enable_fog":{"doc_sha":"ca6c5ffcef454868","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"enable_vertex_snap":{"doc_sha":"7401ad5dcd82024c","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"entities":{"doc_sha":"c2b185a3b718cc6b","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"find_path":{"doc_sha":"ebd174d40557cc19","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"find_path(start, end) -> list"},"fog_color":{"doc_sha":"3eb0cdbd75c5a94c","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"fog_far":{"doc_sha":"90406f8cae40bb58","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"fog_near":{"doc_sha":"e49f6dc79b22a794","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"follow":{"doc_sha":"975d2b945069be26","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"follow(entity, distance=10, height=5, smoothing=1.0)"},"fov":{"doc_sha":"7f5d69a1c423f69d","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"get_billboard":{"doc_sha":"38b0410480ab7d4d","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"get_billboard(index) -> Billboard"},"get_layer":{"doc_sha":"92223f274b5312c3","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"get_layer(name) -> dict or None"},"global_bounds":{"doc_sha":"aa4de9a20df55c2b","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"global_position":{"doc_sha":"b8dabbecc9fccd2a","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"grid_size":{"doc_sha":"7b4dbfa47f259f02","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"h":{"doc_sha":"d7308cd01fffb4be","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"hovered":{"doc_sha":"af0d353ce08f7be0","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"is_in_fov":{"doc_sha":"81d50fbbe2df4c8e","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"is_in_fov(x, z) -> bool"},"layer_count":{"doc_sha":"8bfbb28860b18d51","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"layer_count() -> int"},"move":{"doc_sha":"5c74f429970a2d2b","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"move(dx, dy) or (delta) -> None"},"on_click":{"doc_sha":"b3b8de4e294d71b6","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"on_enter":{"doc_sha":"48c996802e262396","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"on_exit":{"doc_sha":"c14ef0d6d6d40b0e","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"on_move":{"doc_sha":"9c3e26818b5b25b0","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"opacity":{"doc_sha":"c1a7ef41e074519e","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"orbit_camera":{"doc_sha":"29e02de46a996495","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"orbit_camera(angle=0, distance=10, height=5)"},"parent":{"doc_sha":"171b0de4e295c55b","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"place_blocking":{"doc_sha":"9252fd1d594a3867","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"place_blocking(grid_pos, footprint, walkable=False, transparent=False)"},"pos":{"doc_sha":"c1693d7dd0fe875d","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"project_all_voxels_to_nav":{"doc_sha":"ac9e0ca27b557d65","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"project_all_voxels_to_nav(headroom=2)"},"project_voxel_to_nav":{"doc_sha":"1fdf58bc686f7511","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"project_voxel_to_nav(voxel_grid, headroom=2)"},"realign":{"doc_sha":"8dfa7333a49aa697","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"realign() -> None"},"remove_billboard":{"doc_sha":"bf68668896f86bb0","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"remove_billboard(billboard)"},"remove_layer":{"doc_sha":"f5fafdfe3d153aab","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"remove_layer(name) -> bool"},"remove_voxel_layer":{"doc_sha":"b88a956cacc361cb","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"remove_voxel_layer(voxel_grid) -> bool"},"render_resolution":{"doc_sha":"4b141c5648005f04","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"resize":{"doc_sha":"005a04f1d129ebe5","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"resize(width, height) or (size) -> None"},"screen_to_world":{"doc_sha":"ba31510737afd516","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"screen_to_world(x, y, y_plane=0.0) -> tuple or None"},"set_grid_size":{"doc_sha":"ee7d1626547614c8","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"set_grid_size(width, depth)"},"set_slope_cost":{"doc_sha":"dac6d3cd319f1016","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"set_slope_cost(max_slope=0.5, cost_multiplier=1.0)"},"visible":{"doc_sha":"49221a4ae830f3b8","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"voxel_layer_count":{"doc_sha":"c1ca5c90881bbaae","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"voxel_layer_count() -> int"},"w":{"doc_sha":"4c5bcc34301022a0","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"x":{"doc_sha":"cd81d9dc457285aa","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"y":{"doc_sha":"406570900a1cf520","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"z_index":{"doc_sha":"328d87ae44d9fb71","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""}}},"VoxelGrid":{"doc_sha":"6706e3f5276ce104","kind":"class","lifecycle":{"since":"0.2.8"},"members":{"add_material":{"doc_sha":"27dec1c992cf7b99","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"add_material(name, color=Color(255,255,255), sprite_index=-1, transparent=False, path_cost=1.0) -> int"},"cell_size":{"doc_sha":"81b167d608b2cec5","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"clear":{"doc_sha":"8ebe20948a33a84d","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"clear() -> None"},"copy_region":{"doc_sha":"e56c5d28d7fd4e86","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"copy_region(min_coord, max_coord) -> VoxelRegion"},"count_material":{"doc_sha":"c9ef38e1f2f29ab8","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"count_material(material) -> int"},"count_non_air":{"doc_sha":"275d512b30f5134a","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"count_non_air() -> int"},"depth":{"doc_sha":"21671acfc312aeeb","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"fill":{"doc_sha":"fff2fcdddba5ee8c","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"fill(material) -> None"},"fill_box":{"doc_sha":"7c3d982a275f095d","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"fill_box(min_coord, max_coord, material) -> None"},"fill_box_hollow":{"doc_sha":"432e8b518feee0a5","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"fill_box_hollow(min_coord, max_coord, material, thickness=1) -> None"},"fill_cylinder":{"doc_sha":"0b2b0b0826381df0","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"fill_cylinder(base_pos, radius, height, material) -> None"},"fill_noise":{"doc_sha":"567355400611d920","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"fill_noise(min_coord, max_coord, material, threshold=0.5, scale=0.1, seed=0) -> None"},"fill_sphere":{"doc_sha":"e6a85d9c72e2ea44","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"fill_sphere(center, radius, material) -> None"},"from_bytes":{"doc_sha":"faefede0aa8ae636","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"from_bytes(data) -> bool"},"get":{"doc_sha":"52cea4645f0ea3d0","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"get(x, y, z) -> int"},"get_material":{"doc_sha":"2a61f6d54086df97","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"get_material(id) -> dict"},"greedy_meshing":{"doc_sha":"17fffa950c043917","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"height":{"doc_sha":"f271e989afe69da3","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"load":{"doc_sha":"5dcaf71057f836a2","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"load(path) -> bool"},"material_count":{"doc_sha":"0ef72bd68ef0c2d0","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"offset":{"doc_sha":"75230579ca06d8f5","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"paste_region":{"doc_sha":"1f44bacefd614cca","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"paste_region(region, position, skip_air=True) -> None"},"project_column":{"doc_sha":"9bacd59e3a7c5758","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"project_column(x, z, headroom=2) -> dict"},"rebuild_mesh":{"doc_sha":"fa60051037c3328c","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"rebuild_mesh() -> None"},"rotation":{"doc_sha":"5b603e8c652baf50","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"save":{"doc_sha":"bb7090694292f8e3","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"save(path) -> bool"},"set":{"doc_sha":"e9223501162aa9c6","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"set(x, y, z, material) -> None"},"size":{"doc_sha":"2d283d0584913b16","kind":"property","lifecycle":{"since":"0.2.8"},"signature":"Dimensions (width, height, depth) of the grid. Read-only."},"to_bytes":{"doc_sha":"f9a751e75bfc0478","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"to_bytes() -> bytes"},"vertex_count":{"doc_sha":"4df863704c32ecb7","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"visible":{"doc_sha":"7ff1afdbd4bf86a3","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"width":{"doc_sha":"0977aff160584918","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""}}},"VoxelRegion":{"doc_sha":"0cda0630ba7cafef","kind":"class","lifecycle":{"since":"0.2.8"},"members":{"depth":{"doc_sha":"05b3223f7df8ab65","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"height":{"doc_sha":"04834a15ca591472","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"size":{"doc_sha":"75c252385afbc498","kind":"property","lifecycle":{"since":"0.2.8"},"signature":"Dimensions (width, height, depth) of the region. Read-only."},"width":{"doc_sha":"d10d49e58037417d","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""}}},"WangSet":{"doc_sha":"320e161ef3d08a5d","kind":"class","lifecycle":{"since":"0.2.8"},"members":{"apply":{"doc_sha":"2aa8c0ba2d6549d2","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"apply(discrete_map: DiscreteMap, tile_layer: TileLayer) -> None"},"color_count":{"doc_sha":"13b244a538aec70f","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"colors":{"doc_sha":"6d84b3c2dd048b1a","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"name":{"doc_sha":"8b4560b705e5724f","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"resolve":{"doc_sha":"fe2fa09430c4bda6","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"resolve(discrete_map: DiscreteMap) -> list[int]"},"terrain_enum":{"doc_sha":"b8c46ec1a2b03eb6","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"terrain_enum() -> IntEnum"},"type":{"doc_sha":"ea3706e2c65ba7d3","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""}}},"Window":{"doc_sha":"6021e9b58544c378","kind":"class","lifecycle":{"since":"0.2.8"},"members":{"center":{"doc_sha":"91fe1b0d6ca2e4f1","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"center() -> None"},"framerate_limit":{"doc_sha":"de182ed271fbb4d3","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"fullscreen":{"doc_sha":"a517f5331026f2c4","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"game_resolution":{"doc_sha":"3a0dba59fecf5a95","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"get":{"doc_sha":"544bdc10ab93d067","kind":"classmethod","lifecycle":{"since":"0.2.8"},"signature":"get() -> Window"},"resolution":{"doc_sha":"548abee10c4fbcaf","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"scaling_mode":{"doc_sha":"57e03f9ffc9291ae","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"screenshot":{"doc_sha":"5102ab9c4c2c8358","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"screenshot(filename: str = None) -> bytes | None"},"title":{"doc_sha":"0d98279e5ec1b8b1","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"visible":{"doc_sha":"bad7f81ec2e48a31","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"vsync":{"doc_sha":"0335eb1fba386e37","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""}}}},"schema":1,"seeded":false,"version":"0.2.8-dev"} +{"commit":"a7ba486","dirty":true,"functions":{"bresenham":{"doc_sha":"ee2d0f7a54a664f5","kind":"function","lifecycle":{"since":"0.2.8"},"signature":"bresenham(start, end, include_start=True, include_end=True) -> list[tuple[int, int]]"},"end_benchmark":{"doc_sha":"5d783c5dabb7a4e9","kind":"function","lifecycle":{"since":"0.2.8"},"signature":"end_benchmark() -> str"},"exit":{"doc_sha":"7572d4d6b1a1c8a4","kind":"function","lifecycle":{"since":"0.2.8"},"signature":"exit() -> None"},"find":{"doc_sha":"364b95807edcf8b4","kind":"function","lifecycle":{"since":"0.2.8"},"signature":"find(name: str, scene: str = None) -> UIDrawable | None"},"find_all":{"doc_sha":"b4a278f16b172131","kind":"function","lifecycle":{"since":"0.2.8"},"signature":"find_all(pattern: str, scene: str = None) -> list"},"get_metrics":{"doc_sha":"a0bbee3858baa4a9","kind":"function","lifecycle":{"modified":{"commit":"464af6a","version":"0.2.8-dev"},"since":"0.2.8"},"signature":"get_metrics() -> dict"},"lock":{"doc_sha":"e42989cabc81d080","kind":"function","lifecycle":{"since":"0.2.8"},"signature":"lock() -> _LockContext"},"log_benchmark":{"doc_sha":"4564bf7321366a0a","kind":"function","lifecycle":{"since":"0.2.8"},"signature":"log_benchmark(message: str) -> None"},"set_dev_console":{"doc_sha":"940ed19ff4f5243f","kind":"function","lifecycle":{"since":"0.2.8"},"signature":"set_dev_console(enabled: bool) -> None"},"set_scale":{"doc_sha":"b565b441a326af48","kind":"function","lifecycle":{"since":"0.2.8"},"signature":"set_scale(multiplier: float) -> None"},"start_benchmark":{"doc_sha":"783957f3517518fe","kind":"function","lifecycle":{"since":"0.2.8"},"signature":"start_benchmark() -> None"},"step":{"doc_sha":"94e778b7e791028a","kind":"function","lifecycle":{"modified":{"commit":"a7ba486","version":"0.2.8-dev"},"since":"0.2.8"},"signature":"step(dt: float = None) -> float"}},"module_attributes":{"animations":{"doc_sha":"30d25725feac9eb6","kind":"attribute","lifecycle":{"since":"0.2.8-dev"},"readonly":true},"current_scene":{"doc_sha":"ab496ea99fec2fa8","kind":"attribute","lifecycle":{"since":"0.2.8-dev"},"readonly":false},"default_transition":{"doc_sha":"73fb568e9cb25540","kind":"attribute","lifecycle":{"since":"0.2.8-dev"},"readonly":false},"default_transition_duration":{"doc_sha":"ae57530eded49138","kind":"attribute","lifecycle":{"since":"0.2.8-dev"},"readonly":false},"save_dir":{"doc_sha":"96ffa5a27d178b14","kind":"attribute","lifecycle":{"since":"0.2.8-dev"},"readonly":true},"scenes":{"doc_sha":"8021c54383d3cb8e","kind":"attribute","lifecycle":{"since":"0.2.8-dev"},"readonly":true},"timers":{"doc_sha":"48e2aa7747d8f527","kind":"attribute","lifecycle":{"since":"0.2.8-dev"},"readonly":true}},"objects":{"AStarPath":{"doc_sha":"60dd12bb4a54f3dd","kind":"class","lifecycle":{"since":"0.2.8"},"members":{"destination":{"doc_sha":"62a81c7267ad9ed9","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"origin":{"doc_sha":"c7dd130b8d4ef800","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"peek":{"doc_sha":"14054f10b4d9dda0","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"peek() -> Vector"},"remaining":{"doc_sha":"c193adc4bbfa3564","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"walk":{"doc_sha":"3fb64b239109d301","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"walk() -> Vector"}}},"Alignment":{"doc_sha":"77d93d03c8994f84","kind":"class","lifecycle":{"since":"0.2.8"},"members":{"as_integer_ratio":{"doc_sha":"3eb115fab0d0e0ec","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"($self, /)"},"bit_count":{"doc_sha":"f276da700b5a2e37","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"($self, /)"},"bit_length":{"doc_sha":"15b29db62fd0fa6c","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"($self, /)"},"conjugate":{"doc_sha":"6517a002faad7c75","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"($self, /)"},"denominator":{"doc_sha":"5051eb1570768851","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"from_bytes":{"doc_sha":"64672607865643a7","kind":"classmethod","lifecycle":{"since":"0.2.8"},"signature":"($type, /, bytes, byteorder='big', *, signed=False)"},"imag":{"doc_sha":"ef78ea410df39b26","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"is_integer":{"doc_sha":"6176763fbac2fd22","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"($self, /)"},"numerator":{"doc_sha":"ed255748ce2eda7c","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"real":{"doc_sha":"d44497f1990af6fe","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"to_bytes":{"doc_sha":"b452319e48e64764","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"($self, /, length=1, byteorder='big', *, signed=False)"}}},"Arc":{"doc_sha":"8323f4b7101509e9","kind":"class","lifecycle":{"since":"0.2.8"},"members":{"align":{"doc_sha":"58657c3cfa010f31","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"animate":{"doc_sha":"400df4f5893261c5","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"animate(property: str, target: Any, duration: float, easing=None, delta=False, loop=False, callback=None, conflict_mode='replace') -> Animation"},"bounds":{"doc_sha":"d3186ac3b3ff8bed","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"center":{"doc_sha":"abf1643dda106bf8","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"color":{"doc_sha":"bdf932405b56c319","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"end_angle":{"doc_sha":"74f9704909a3a23d","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"global_bounds":{"doc_sha":"aa4de9a20df55c2b","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"global_position":{"doc_sha":"b8dabbecc9fccd2a","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"grid_pos":{"doc_sha":"9df77e9a996208ba","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"grid_size":{"doc_sha":"e32208817299dfe9","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"horiz_margin":{"doc_sha":"0ef227a4cae6e675","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"hovered":{"doc_sha":"af0d353ce08f7be0","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"margin":{"doc_sha":"ab774a7da8cfe9bf","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"move":{"doc_sha":"5c74f429970a2d2b","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"move(dx, dy) or (delta) -> None"},"name":{"doc_sha":"0cc0684f26d8a660","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"on_click":{"doc_sha":"f2ef657aaf8b6c68","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"on_enter":{"doc_sha":"48c996802e262396","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"on_exit":{"doc_sha":"c14ef0d6d6d40b0e","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"on_move":{"doc_sha":"9c3e26818b5b25b0","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"opacity":{"doc_sha":"c1a7ef41e074519e","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"origin":{"doc_sha":"270bdf366c74bb1d","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"parent":{"doc_sha":"171b0de4e295c55b","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"pos":{"doc_sha":"6612fa35c43918e7","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"radius":{"doc_sha":"f9c2310b775f1e5a","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"realign":{"doc_sha":"8dfa7333a49aa697","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"realign() -> None"},"resize":{"doc_sha":"005a04f1d129ebe5","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"resize(width, height) or (size) -> None"},"rotate_with_camera":{"doc_sha":"5c3af880218d3a08","kind":"property","lifecycle":{"modified":{"commit":"4bb1966","version":"0.2.8-dev"},"since":"0.2.8"},"signature":""},"rotation":{"doc_sha":"61ba3e66a0841a08","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"start_angle":{"doc_sha":"6b91f175ebe5ecb4","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"thickness":{"doc_sha":"f8eba89682554e6d","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"vert_margin":{"doc_sha":"e2042be923bc6fcb","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"visible":{"doc_sha":"49221a4ae830f3b8","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"z_index":{"doc_sha":"67dd517aa153c158","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""}}},"AutoRuleSet":{"doc_sha":"49ab716632a44f06","kind":"class","lifecycle":{"since":"0.2.8"},"members":{"apply":{"doc_sha":"701b6f038cfd6809","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"apply(discrete_map: DiscreteMap, tile_layer: TileLayer, seed: int = 0) -> None"},"grid_size":{"doc_sha":"570f4c81acb781d8","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"group_count":{"doc_sha":"892fc283939b3e0c","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"name":{"doc_sha":"4f5ca90860649b64","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"resolve":{"doc_sha":"f8fff4e6e6488e3f","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"resolve(discrete_map: DiscreteMap, seed: int = 0) -> list[int]"},"rule_count":{"doc_sha":"9b9d5a7c1475dcfa","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"terrain_enum":{"doc_sha":"eb365bf99bbcca37","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"terrain_enum() -> IntEnum"},"value_count":{"doc_sha":"3467d158dd5af073","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"values":{"doc_sha":"b3199e059fd1ca2a","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""}}},"BSP":{"doc_sha":"7e971e0542cf4006","kind":"class","lifecycle":{"since":"0.2.8"},"members":{"adjacency":{"doc_sha":"9373aa85408486ba","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"bounds":{"doc_sha":"9c41e7403f2a0b2f","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"clear":{"doc_sha":"2e4ffd3360ac858e","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"clear() -> BSP"},"find":{"doc_sha":"fccfac484a2b889c","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"find(pos: tuple[int, int] | list | Vector) -> BSPNode | None"},"get_leaf":{"doc_sha":"d8ba680ec2ad4fd1","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"get_leaf(index: int) -> BSPNode"},"leaves":{"doc_sha":"439ec78ec02e6df8","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"leaves() -> Iterator[BSPNode]"},"pos":{"doc_sha":"b8b27f4ad76e2870","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"root":{"doc_sha":"d1e3d9741dab383d","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"size":{"doc_sha":"621f11f9a37dd89d","kind":"property","lifecycle":{"since":"0.2.8"},"signature":"Dimensions (width, height). Read-only."},"split_once":{"doc_sha":"0d7c0c33156bc4e5","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"split_once(horizontal: bool, position: int) -> BSP"},"split_recursive":{"doc_sha":"fa516b79833e5094","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"split_recursive(depth: int, min_size: tuple[int, int], max_ratio: float = 1.5, seed: int | None = None) -> BSP"},"to_heightmap":{"doc_sha":"a0f03cab7a0f2d1b","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"to_heightmap(size: tuple[int, int] | None = None, select: str = 'leaves', shrink: int = 0, value: float = 1.0) -> HeightMap"},"traverse":{"doc_sha":"57da83e0f37686aa","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"traverse(order: Traversal = Traversal.LEVEL_ORDER) -> Iterator[BSPNode]"}}},"Behavior":{"doc_sha":"09206365e6c37ecc","kind":"class","lifecycle":{"since":"0.2.8"},"members":{"as_integer_ratio":{"doc_sha":"3eb115fab0d0e0ec","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"($self, /)"},"bit_count":{"doc_sha":"f276da700b5a2e37","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"($self, /)"},"bit_length":{"doc_sha":"15b29db62fd0fa6c","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"($self, /)"},"conjugate":{"doc_sha":"6517a002faad7c75","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"($self, /)"},"denominator":{"doc_sha":"5051eb1570768851","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"from_bytes":{"doc_sha":"64672607865643a7","kind":"classmethod","lifecycle":{"since":"0.2.8"},"signature":"($type, /, bytes, byteorder='big', *, signed=False)"},"imag":{"doc_sha":"ef78ea410df39b26","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"is_integer":{"doc_sha":"6176763fbac2fd22","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"($self, /)"},"numerator":{"doc_sha":"ed255748ce2eda7c","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"real":{"doc_sha":"d44497f1990af6fe","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"to_bytes":{"doc_sha":"b452319e48e64764","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"($self, /, length=1, byteorder='big', *, signed=False)"}}},"Billboard":{"doc_sha":"38614d1d19942bab","kind":"class","lifecycle":{"since":"0.2.8"},"members":{"facing":{"doc_sha":"f82e2d7d8087804a","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"opacity":{"doc_sha":"dc5ed39915ab896c","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"phi":{"doc_sha":"1450c89fb5b47929","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"pos":{"doc_sha":"0d5fec7fd4701ea6","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"scale":{"doc_sha":"c9d55c4579681854","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"sprite_index":{"doc_sha":"170c20c841b586fc","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"texture":{"doc_sha":"b6d846566b6ccf93","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"theta":{"doc_sha":"c214ecbcb099228b","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"visible":{"doc_sha":"adf8070be9a09eba","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""}}},"CallableBinding":{"doc_sha":"711bf0f149ccae7a","kind":"class","lifecycle":{"since":"0.2.8"},"members":{"callable":{"doc_sha":"96d3d31a6b7c56d9","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"is_valid":{"doc_sha":"bc69c90e13475ae6","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"value":{"doc_sha":"8f7d9eb4f520c017","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""}}},"Caption":{"doc_sha":"8cd46324baa46a0d","kind":"class","lifecycle":{"since":"0.2.8"},"members":{"align":{"doc_sha":"58657c3cfa010f31","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"animate":{"doc_sha":"400df4f5893261c5","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"animate(property: str, target: Any, duration: float, easing=None, delta=False, loop=False, callback=None, conflict_mode='replace') -> Animation"},"bounds":{"doc_sha":"d3186ac3b3ff8bed","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"fill_color":{"doc_sha":"af7ba90df95e4b79","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"font":{"doc_sha":"fbdafdb33921aa13","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"font_size":{"doc_sha":"7894149ec4efec6e","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"global_bounds":{"doc_sha":"aa4de9a20df55c2b","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"global_position":{"doc_sha":"b8dabbecc9fccd2a","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"grid_pos":{"doc_sha":"79a7d48503e657d1","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"grid_size":{"doc_sha":"25b029c86a618fee","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"h":{"doc_sha":"ca1a98d700ff1888","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"horiz_margin":{"doc_sha":"0ef227a4cae6e675","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"hovered":{"doc_sha":"af0d353ce08f7be0","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"margin":{"doc_sha":"ab774a7da8cfe9bf","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"move":{"doc_sha":"5c74f429970a2d2b","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"move(dx, dy) or (delta) -> None"},"name":{"doc_sha":"90a2ab0ec07c00eb","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"on_click":{"doc_sha":"b3b8de4e294d71b6","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"on_enter":{"doc_sha":"48c996802e262396","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"on_exit":{"doc_sha":"c14ef0d6d6d40b0e","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"on_move":{"doc_sha":"9c3e26818b5b25b0","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"opacity":{"doc_sha":"c1a7ef41e074519e","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"origin":{"doc_sha":"270bdf366c74bb1d","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"outline":{"doc_sha":"6580b31092048bcb","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"outline_color":{"doc_sha":"388919803d270fd3","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"parent":{"doc_sha":"171b0de4e295c55b","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"pos":{"doc_sha":"44e4967e15212959","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"realign":{"doc_sha":"8dfa7333a49aa697","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"realign() -> None"},"resize":{"doc_sha":"005a04f1d129ebe5","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"resize(width, height) or (size) -> None"},"rotate_with_camera":{"doc_sha":"5c3af880218d3a08","kind":"property","lifecycle":{"modified":{"commit":"4bb1966","version":"0.2.8-dev"},"since":"0.2.8"},"signature":""},"rotation":{"doc_sha":"61ba3e66a0841a08","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"shader":{"doc_sha":"418952064319ffb5","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"size":{"doc_sha":"85abb564299b703d","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"text":{"doc_sha":"41628142b287defe","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"uniforms":{"doc_sha":"b1f2699da618d002","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"vert_margin":{"doc_sha":"e2042be923bc6fcb","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"visible":{"doc_sha":"49221a4ae830f3b8","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"w":{"doc_sha":"8472810bca891509","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"x":{"doc_sha":"528487f8deaaf789","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"y":{"doc_sha":"114ecec940477107","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"z_index":{"doc_sha":"328d87ae44d9fb71","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""}}},"Circle":{"doc_sha":"077981a1fbdfb6e3","kind":"class","lifecycle":{"since":"0.2.8"},"members":{"align":{"doc_sha":"58657c3cfa010f31","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"animate":{"doc_sha":"400df4f5893261c5","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"animate(property: str, target: Any, duration: float, easing=None, delta=False, loop=False, callback=None, conflict_mode='replace') -> Animation"},"bounds":{"doc_sha":"d3186ac3b3ff8bed","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"center":{"doc_sha":"d1e80ccd6bf706b9","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"fill_color":{"doc_sha":"46be64336e296c50","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"global_bounds":{"doc_sha":"aa4de9a20df55c2b","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"global_position":{"doc_sha":"b8dabbecc9fccd2a","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"grid_pos":{"doc_sha":"20bebfa7432ed339","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"grid_size":{"doc_sha":"b43e33c2d49a3565","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"horiz_margin":{"doc_sha":"0ef227a4cae6e675","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"hovered":{"doc_sha":"af0d353ce08f7be0","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"margin":{"doc_sha":"ab774a7da8cfe9bf","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"move":{"doc_sha":"5c74f429970a2d2b","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"move(dx, dy) or (delta) -> None"},"name":{"doc_sha":"0cc0684f26d8a660","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"on_click":{"doc_sha":"3a340282000297e0","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"on_enter":{"doc_sha":"48c996802e262396","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"on_exit":{"doc_sha":"c14ef0d6d6d40b0e","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"on_move":{"doc_sha":"9c3e26818b5b25b0","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"opacity":{"doc_sha":"c1a7ef41e074519e","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"origin":{"doc_sha":"270bdf366c74bb1d","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"outline":{"doc_sha":"20e9fd42a7ba4bb8","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"outline_color":{"doc_sha":"3fbed883fee308fe","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"parent":{"doc_sha":"171b0de4e295c55b","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"pos":{"doc_sha":"5f9d88ff075d2cfc","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"radius":{"doc_sha":"00a41207192c55a8","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"realign":{"doc_sha":"8dfa7333a49aa697","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"realign() -> None"},"resize":{"doc_sha":"005a04f1d129ebe5","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"resize(width, height) or (size) -> None"},"rotate_with_camera":{"doc_sha":"5c3af880218d3a08","kind":"property","lifecycle":{"modified":{"commit":"4bb1966","version":"0.2.8-dev"},"since":"0.2.8"},"signature":""},"rotation":{"doc_sha":"61ba3e66a0841a08","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"vert_margin":{"doc_sha":"e2042be923bc6fcb","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"visible":{"doc_sha":"49221a4ae830f3b8","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"z_index":{"doc_sha":"0cb3df5278d2658b","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""}}},"Color":{"doc_sha":"f1f664405900332f","kind":"class","lifecycle":{"since":"0.2.8"},"members":{"a":{"doc_sha":"ef61630ad34f8e2a","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"b":{"doc_sha":"2e2e91e93833fdd0","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"from_hex":{"doc_sha":"24d1ff553133bb7e","kind":"classmethod","lifecycle":{"since":"0.2.8"},"signature":"from_hex(hex_string: str) -> Color"},"g":{"doc_sha":"716ced374bcc2dc4","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"lerp":{"doc_sha":"290e204f3538e406","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"lerp(other: Color, t: float) -> Color"},"r":{"doc_sha":"ef64cae6296f68cb","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"to_hex":{"doc_sha":"d0cf65b284158877","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"to_hex() -> str"}}},"ColorLayer":{"doc_sha":"f8a87b6184c9a9b6","kind":"class","lifecycle":{"since":"0.2.8"},"members":{"apply_gradient":{"doc_sha":"20390111f0652ae0","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"apply_gradient(source: HeightMap, range: tuple, color_low: Color, color_high: Color) -> ColorLayer"},"apply_perspective":{"doc_sha":"a909e1b1c0373bc0","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"apply_perspective(entity: Entity, visible: Color | None = None, discovered: Color | None = None, unknown: Color | None = None) -> None"},"apply_ranges":{"doc_sha":"37fc5a26fda9cc0c","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"apply_ranges(source: HeightMap, ranges: list) -> ColorLayer"},"apply_threshold":{"doc_sha":"59cc9b72322cc677","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"apply_threshold(source: HeightMap, range: tuple, color: Color) -> ColorLayer"},"at":{"doc_sha":"fef47313dcef9247","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"at(pos: tuple | Vector) or (x: int, y: int) -> Color"},"clear_perspective":{"doc_sha":"fec237fe4cbda340","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"clear_perspective() -> None"},"draw_fov":{"doc_sha":"ff965b12c888d092","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"draw_fov(source: tuple, radius: int | None = None, fov: FOV | None = None, visible: Color | None = None, discovered: Color | None = None, unknown: Color | None = None) -> None"},"edit":{"doc_sha":"b0edb9c8f4c1fe76","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"edit() -> context manager"},"fill":{"doc_sha":"f6ff376fd71ae90b","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"fill(color: Color) -> None"},"fill_rect":{"doc_sha":"bc8b20da214aa37d","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"fill_rect(pos: tuple, size: tuple, color: Color) -> None"},"grid":{"doc_sha":"b965c55d57de78fa","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"grid_size":{"doc_sha":"eac88eb0f7b16cbf","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"name":{"doc_sha":"dd282cc9273e2e36","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"set":{"doc_sha":"36801cc04606ee0b","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"set(pos, color: Color) -> None"},"update_perspective":{"doc_sha":"c9caac507fbc4921","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"update_perspective() -> None"},"visible":{"doc_sha":"ecea81ec16e31c19","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"z_index":{"doc_sha":"eb13c1b4de11ce63","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""}}},"DijkstraMap":{"doc_sha":"822ccf5b086d37ec","kind":"class","lifecycle":{"since":"0.2.8"},"members":{"descent_step":{"doc_sha":"fd7b649f1f3eb586","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"descent_step(pos: Vector | tuple) -> Vector | None"},"distance":{"doc_sha":"92b40108bb47bf88","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"distance(pos: Vector | tuple) -> float | None"},"invert":{"doc_sha":"3e5ae145dafc2c1d","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"invert() -> DijkstraMap"},"path_from":{"doc_sha":"3dcf638bff837f2d","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"path_from(pos: Vector | tuple) -> AStarPath"},"root":{"doc_sha":"95824bac9c46ad83","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"step_from":{"doc_sha":"ac9bc93f671d3936","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"step_from(pos: Vector | tuple) -> Vector | None"},"to_heightmap":{"doc_sha":"0ba542e6509b510b","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"to_heightmap(size=None, unreachable=-1.0) -> HeightMap"}}},"DiscreteMap":{"doc_sha":"4d6d00ffa3ac62a9","kind":"class","lifecycle":{"since":"0.2.8"},"members":{"add":{"doc_sha":"1e5a5b83f555fa87","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"add(other: DiscreteMap | int, *, pos=None, source_pos=None, size=None) -> DiscreteMap"},"bitwise_and":{"doc_sha":"1644af83b8699792","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"bitwise_and(other: DiscreteMap, *, pos=None, source_pos=None, size=None) -> DiscreteMap"},"bitwise_or":{"doc_sha":"c8262af2fdccdeeb","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"bitwise_or(other: DiscreteMap, *, pos=None, source_pos=None, size=None) -> DiscreteMap"},"bitwise_xor":{"doc_sha":"01809471595bf8e7","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"bitwise_xor(other: DiscreteMap, *, pos=None, source_pos=None, size=None) -> DiscreteMap"},"bool":{"doc_sha":"a999f0f09edf7bf3","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"bool(condition: int | set | callable) -> DiscreteMap"},"clear":{"doc_sha":"b04d176e9c7bf9d2","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"clear() -> DiscreteMap"},"copy_from":{"doc_sha":"efea5a0aa9a8d9c9","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"copy_from(other: DiscreteMap, *, pos=None, source_pos=None, size=None) -> DiscreteMap"},"count":{"doc_sha":"5e3303ddc3a9e3ae","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"count(value: int) -> int"},"count_range":{"doc_sha":"ac4e0298919fd16e","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"count_range(min_val: int, max_val: int) -> int"},"enum_type":{"doc_sha":"f27f1ad46e2d5afe","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"fill":{"doc_sha":"7f358bdcc5c2c9f0","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"fill(value: int, *, pos=None, size=None) -> DiscreteMap"},"from_bytes":{"doc_sha":"b1c09c737d43ffa9","kind":"classmethod","lifecycle":{"since":"0.2.8"},"signature":"from_bytes(data: bytes, size: tuple[int, int], *, enum: type = None) -> DiscreteMap"},"from_heightmap":{"doc_sha":"0e89bb4262b19ea3","kind":"classmethod","lifecycle":{"since":"0.2.8"},"signature":"from_heightmap(hmap: HeightMap, mapping: list[tuple[tuple[float,float], int]], *, enum=None) -> DiscreteMap"},"get":{"doc_sha":"a1097b27b43d6a27","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"get(x, y) or (pos) -> int | Enum"},"histogram":{"doc_sha":"b2d9df4f14aedd51","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"histogram() -> dict[int, int]"},"invert":{"doc_sha":"0f52aecfe57ff499","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"invert() -> DiscreteMap"},"mask":{"doc_sha":"0d9628616f4592f4","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"mask() -> memoryview"},"max":{"doc_sha":"144265dd0da983a8","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"max(other: DiscreteMap, *, pos=None, source_pos=None, size=None) -> DiscreteMap"},"min":{"doc_sha":"ff90ceef60a45959","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"min(other: DiscreteMap, *, pos=None, source_pos=None, size=None) -> DiscreteMap"},"min_max":{"doc_sha":"22018caaf1bdb60e","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"min_max() -> tuple[int, int]"},"multiply":{"doc_sha":"430abcb2b647132f","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"multiply(factor: float, *, pos=None, size=None) -> DiscreteMap"},"set":{"doc_sha":"8439c14e5785f7e4","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"set(x: int, y: int, value: int) -> None"},"size":{"doc_sha":"fc6c37c9d3916c5d","kind":"property","lifecycle":{"since":"0.2.8"},"signature":"Dimensions (width, height) of the map. Read-only."},"subtract":{"doc_sha":"aeae8042663b9973","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"subtract(other: DiscreteMap | int, *, pos=None, source_pos=None, size=None) -> DiscreteMap"},"to_bytes":{"doc_sha":"4d7c4b5939a546bd","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"to_bytes() -> bytes"},"to_heightmap":{"doc_sha":"b0744db8f889b5d9","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"to_heightmap(mapping: dict[int, float] = None) -> HeightMap"}}},"Drawable":{"doc_sha":"bb28a8d677f24b33","kind":"class","lifecycle":{"since":"0.2.8"},"members":{"move":{"doc_sha":"4a8f9ca3692c6b58","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"move(dx, dy) or (delta) -> None"},"on_click":{"doc_sha":"b3b8de4e294d71b6","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"opacity":{"doc_sha":"c1a7ef41e074519e","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"resize":{"doc_sha":"980bdc9065fc077b","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"resize(width, height) or (size) -> None"},"visible":{"doc_sha":"49221a4ae830f3b8","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"z_index":{"doc_sha":"328d87ae44d9fb71","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""}}},"Easing":{"doc_sha":"e3b0c44298fc1c14","kind":"class","lifecycle":{"since":"0.2.8"},"members":{"as_integer_ratio":{"doc_sha":"3eb115fab0d0e0ec","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"($self, /)"},"bit_count":{"doc_sha":"f276da700b5a2e37","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"($self, /)"},"bit_length":{"doc_sha":"15b29db62fd0fa6c","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"($self, /)"},"conjugate":{"doc_sha":"6517a002faad7c75","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"($self, /)"},"denominator":{"doc_sha":"5051eb1570768851","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"from_bytes":{"doc_sha":"64672607865643a7","kind":"classmethod","lifecycle":{"since":"0.2.8"},"signature":"($type, /, bytes, byteorder='big', *, signed=False)"},"imag":{"doc_sha":"ef78ea410df39b26","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"is_integer":{"doc_sha":"6176763fbac2fd22","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"($self, /)"},"numerator":{"doc_sha":"ed255748ce2eda7c","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"real":{"doc_sha":"d44497f1990af6fe","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"to_bytes":{"doc_sha":"b452319e48e64764","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"($self, /, length=1, byteorder='big', *, signed=False)"}}},"Entity":{"doc_sha":"7231aaf2e3b107d0","kind":"class","lifecycle":{"since":"0.2.8"},"members":{"add_label":{"doc_sha":"be06d30bbd5a41ca","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"add_label(label: str) -> None"},"animate":{"doc_sha":"a06820cecef57a9a","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"animate(property: str, target: Any, duration: float, easing=None, delta=False, loop=False, callback=None, conflict_mode='replace') -> Animation"},"at":{"doc_sha":"b5775513388a1464","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"at(x: int, y: int) -> GridPoint | None"},"behavior_type":{"doc_sha":"745936b7029480fd","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"cell_pos":{"doc_sha":"f8feaba9f3652ca0","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"cell_x":{"doc_sha":"8bdc1199bfb15967","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"cell_y":{"doc_sha":"8fd78a55ede4b259","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"default_behavior":{"doc_sha":"f0a1ea99426408dd","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"die":{"doc_sha":"1369fecbc5a1862a","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"die() -> None"},"draw_pos":{"doc_sha":"2769bf67eec13406","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"find_path":{"doc_sha":"1a672b91a00eb597","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"find_path(target, diagonal_cost: float = 1.41, collide: str = None) -> AStarPath | None"},"grid":{"doc_sha":"d02c7b8c3beb5662","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"grid_pos":{"doc_sha":"d2e9f06a9d9f0a3c","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"grid_x":{"doc_sha":"4108b9ec31aff2c0","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"grid_y":{"doc_sha":"26f7190f34a208b2","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"has_label":{"doc_sha":"0e65dc586b3c8d18","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"has_label(label: str) -> bool"},"index":{"doc_sha":"74e24820bb380594","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"index() -> int"},"labels":{"doc_sha":"4e16ef3312d5b4e7","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"move":{"doc_sha":"5c74f429970a2d2b","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"move(dx, dy) or (delta) -> None"},"move_speed":{"doc_sha":"a3b3867a6d3b830a","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"name":{"doc_sha":"708f75f696069d69","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"opacity":{"doc_sha":"6caae59e941bebf1","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"path_to":{"doc_sha":"5b11e33c92caac2b","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"path_to(x: int, y: int) -> list"},"perspective_map":{"doc_sha":"f61894673d8c27a7","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"pos":{"doc_sha":"172ff9a95e621eaf","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"remove_label":{"doc_sha":"a4a541d7e9cf7275","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"remove_label(label: str) -> None"},"resize":{"doc_sha":"005a04f1d129ebe5","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"resize(width, height) or (size) -> None"},"set_behavior":{"doc_sha":"328826510c72f049","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"set_behavior(type, waypoints=None, turns: int = 0, path=None, pathfinder=None) -> None"},"shader":{"doc_sha":"04503c544358f827","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"sight_radius":{"doc_sha":"4a9ec6c3788b093a","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"sprite_grid":{"doc_sha":"a5ff2c3e5190bc41","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"sprite_index":{"doc_sha":"ca285ed1c6f460df","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"sprite_offset":{"doc_sha":"a92dae4a2555fa7b","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"sprite_offset_x":{"doc_sha":"7ba6504a9bbbf681","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"sprite_offset_y":{"doc_sha":"24d4546e182bb2c8","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"step":{"doc_sha":"8877861183e50020","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"target_label":{"doc_sha":"85acb11a7e713a41","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"texture":{"doc_sha":"1588a0d4779fd069","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"tile_height":{"doc_sha":"7f2ef7cb6d425730","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"tile_size":{"doc_sha":"1a647ababfde2daa","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"tile_width":{"doc_sha":"5d72ddf874e9db54","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"turn_order":{"doc_sha":"eba665bc9c7e75ee","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"uniforms":{"doc_sha":"d302457c5aaa495e","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"update_visibility":{"doc_sha":"d096cd998ed99b49","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"update_visibility() -> None"},"visible":{"doc_sha":"2bca3a265b44b270","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"visible_entities":{"doc_sha":"2482fbbecbfac7ea","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"visible_entities(fov=None, radius: int | None = None) -> list[Entity]"},"x":{"doc_sha":"4ba6a677ea35e7ba","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"y":{"doc_sha":"52b85f7dc8a7cb27","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""}}},"Entity3D":{"doc_sha":"0daee46cf6343e9c","kind":"class","lifecycle":{"since":"0.2.8"},"members":{"anim_clip":{"doc_sha":"f69294fc08fc1d6c","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"anim_frame":{"doc_sha":"41bdd9b6f4407d16","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"anim_loop":{"doc_sha":"b267a372c1aef2cd","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"anim_paused":{"doc_sha":"e100f0be03f09b09","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"anim_speed":{"doc_sha":"33c581c9533b5169","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"anim_time":{"doc_sha":"6b811c83dd2a2184","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"animate":{"doc_sha":"fcf44af080ce16a7","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"animate(property, target, duration, easing=None, delta=False, callback=None, conflict_mode=None)"},"at":{"doc_sha":"69d6101c79794f90","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"at(x, z) -> dict"},"auto_animate":{"doc_sha":"867ca7a9c2b5a4ab","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"clear_path":{"doc_sha":"989bb0a431b58e17","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"clear_path()"},"color":{"doc_sha":"ea9ab21659b6e392","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"follow_path":{"doc_sha":"f514b7092f561550","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"follow_path(path)"},"grid_pos":{"doc_sha":"f20411521f420193","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"idle_clip":{"doc_sha":"855c97611053652c","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"is_moving":{"doc_sha":"b31c1090228993e4","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"model":{"doc_sha":"26ae36cf867d07e9","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"name":{"doc_sha":"acea3a6577d65fbc","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"on_anim_complete":{"doc_sha":"5cc47894576d983d","kind":"property","lifecycle":{"since":"0.2.8"},"signature":"Callback(entity, clip_name) when non-looping animation ends."},"path_to":{"doc_sha":"d6fe3936cc8e2d84","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"path_to(x, z) or path_to(pos=(x, z)) -> list"},"pos":{"doc_sha":"2488492030e9df40","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"rotation":{"doc_sha":"5b603e8c652baf50","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"scale":{"doc_sha":"b06782214d92e3f7","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"teleport":{"doc_sha":"d53364830cdc737a","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"teleport(x, z) or teleport(pos=(x, z))"},"update_visibility":{"doc_sha":"61886fb70daedf6b","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"update_visibility()"},"viewport":{"doc_sha":"e9f9154f00b47f9a","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"visible":{"doc_sha":"bc1ae0f4c6b5cb11","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"walk_clip":{"doc_sha":"c95d11b4e1901f28","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"world_pos":{"doc_sha":"bcc6673be9243dd5","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""}}},"EntityCollection3D":{"doc_sha":"55864ac691527ca9","kind":"class","lifecycle":{"since":"0.2.8"},"members":{"append":{"doc_sha":"43922f93aff7d723","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"append(entity)"},"clear":{"doc_sha":"f44ea077b10186d7","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"clear()"},"extend":{"doc_sha":"4fd2e7e83437c7ae","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"extend(iterable)"},"find":{"doc_sha":"e4182a3a3317aaae","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"find(name) -> Entity3D or None"},"pop":{"doc_sha":"cbcc90e2a433f277","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"pop(index=-1) -> Entity3D"},"remove":{"doc_sha":"b2f8fb276b91ec1e","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"remove(entity)"}}},"EntityCollection3DIter":{"doc_sha":"d71ec48b224bfacc","kind":"class","lifecycle":{"since":"0.2.8"},"members":{}},"FOV":{"doc_sha":"e3b0c44298fc1c14","kind":"class","lifecycle":{"since":"0.2.8"},"members":{"as_integer_ratio":{"doc_sha":"3eb115fab0d0e0ec","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"($self, /)"},"bit_count":{"doc_sha":"f276da700b5a2e37","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"($self, /)"},"bit_length":{"doc_sha":"15b29db62fd0fa6c","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"($self, /)"},"conjugate":{"doc_sha":"6517a002faad7c75","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"($self, /)"},"denominator":{"doc_sha":"5051eb1570768851","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"from_bytes":{"doc_sha":"64672607865643a7","kind":"classmethod","lifecycle":{"since":"0.2.8"},"signature":"($type, /, bytes, byteorder='big', *, signed=False)"},"imag":{"doc_sha":"ef78ea410df39b26","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"is_integer":{"doc_sha":"6176763fbac2fd22","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"($self, /)"},"numerator":{"doc_sha":"ed255748ce2eda7c","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"real":{"doc_sha":"d44497f1990af6fe","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"to_bytes":{"doc_sha":"b452319e48e64764","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"($self, /, length=1, byteorder='big', *, signed=False)"}}},"Font":{"doc_sha":"245b4e9b0ca66fc9","kind":"class","lifecycle":{"since":"0.2.8"},"members":{"family":{"doc_sha":"0cf26d3038432764","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"source":{"doc_sha":"efc970576a584cde","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""}}},"Frame":{"doc_sha":"4d53c3cb9e9e4611","kind":"class","lifecycle":{"since":"0.2.8"},"members":{"align":{"doc_sha":"58657c3cfa010f31","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"animate":{"doc_sha":"400df4f5893261c5","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"animate(property: str, target: Any, duration: float, easing=None, delta=False, loop=False, callback=None, conflict_mode='replace') -> Animation"},"bounds":{"doc_sha":"d3186ac3b3ff8bed","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"cache_subtree":{"doc_sha":"0b8d1175192c1ce2","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"children":{"doc_sha":"b2cf42011b86727c","kind":"property","lifecycle":{"modified":{"commit":"4c0953c","version":"0.2.8-dev"},"since":"0.2.8"},"signature":""},"clip_children":{"doc_sha":"d12b2c4d1d2bb6cb","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"fill_color":{"doc_sha":"8cfbe5aa6dd6da27","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"global_bounds":{"doc_sha":"aa4de9a20df55c2b","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"global_position":{"doc_sha":"b8dabbecc9fccd2a","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"grid_pos":{"doc_sha":"2f5b05882c4ad39f","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"grid_size":{"doc_sha":"9c8707ad589e5d8a","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"h":{"doc_sha":"ced8010da828cc93","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"horiz_margin":{"doc_sha":"0ef227a4cae6e675","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"hovered":{"doc_sha":"af0d353ce08f7be0","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"margin":{"doc_sha":"ab774a7da8cfe9bf","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"move":{"doc_sha":"5c74f429970a2d2b","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"move(dx, dy) or (delta) -> None"},"name":{"doc_sha":"90a2ab0ec07c00eb","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"on_click":{"doc_sha":"b3b8de4e294d71b6","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"on_enter":{"doc_sha":"48c996802e262396","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"on_exit":{"doc_sha":"c14ef0d6d6d40b0e","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"on_move":{"doc_sha":"9c3e26818b5b25b0","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"opacity":{"doc_sha":"c1a7ef41e074519e","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"origin":{"doc_sha":"270bdf366c74bb1d","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"outline":{"doc_sha":"c07b6dec9da5b9ea","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"outline_color":{"doc_sha":"cd98fcaf39f0bc56","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"parent":{"doc_sha":"171b0de4e295c55b","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"pos":{"doc_sha":"55a118e7889fd555","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"realign":{"doc_sha":"8dfa7333a49aa697","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"realign() -> None"},"resize":{"doc_sha":"005a04f1d129ebe5","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"resize(width, height) or (size) -> None"},"rotate_with_camera":{"doc_sha":"5c3af880218d3a08","kind":"property","lifecycle":{"modified":{"commit":"4bb1966","version":"0.2.8-dev"},"since":"0.2.8"},"signature":""},"rotation":{"doc_sha":"61ba3e66a0841a08","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"shader":{"doc_sha":"418952064319ffb5","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"uniforms":{"doc_sha":"b1f2699da618d002","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"vert_margin":{"doc_sha":"e2042be923bc6fcb","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"visible":{"doc_sha":"49221a4ae830f3b8","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"w":{"doc_sha":"bd51299002a099c5","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"x":{"doc_sha":"4ed17c625f2289e3","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"y":{"doc_sha":"fabaa45fd789c7fd","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"z_index":{"doc_sha":"328d87ae44d9fb71","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""}}},"Grid":{"doc_sha":"f88e43e992d43a86","kind":"class","lifecycle":{"since":"0.2.8"},"members":{"align":{"doc_sha":"58657c3cfa010f31","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"animate":{"doc_sha":"400df4f5893261c5","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"animate(property: str, target: Any, duration: float, easing=None, delta=False, loop=False, callback=None, conflict_mode='replace') -> Animation"},"bounds":{"doc_sha":"d3186ac3b3ff8bed","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"camera_rotation":{"doc_sha":"bc2cd502f52817e9","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"center":{"doc_sha":"913ef5c36eaf9635","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"center_camera":{"doc_sha":"00d37ccdedcd3a2e","kind":"method","lifecycle":{"since":"0.2.8-dev"},"signature":"center_camera(pos: tuple = None) -> None"},"center_x":{"doc_sha":"fbb0ff08116f3c80","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"center_y":{"doc_sha":"1bda27633025fb12","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"children":{"doc_sha":"9566d92d5619ee21","kind":"property","lifecycle":{"since":"0.2.8-dev"},"signature":""},"fill_color":{"doc_sha":"f26fb5da3de1789a","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"global_bounds":{"doc_sha":"aa4de9a20df55c2b","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"global_position":{"doc_sha":"b8dabbecc9fccd2a","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"grid_data":{"doc_sha":"fdf0084b8456fd45","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"h":{"doc_sha":"bd5a06618a50efad","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"horiz_margin":{"doc_sha":"0ef227a4cae6e675","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"hovered":{"doc_sha":"af0d353ce08f7be0","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"hovered_cell":{"doc_sha":"85c87c8aa013d0c4","kind":"property","lifecycle":{"since":"0.2.8-dev"},"signature":""},"margin":{"doc_sha":"ab774a7da8cfe9bf","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"move":{"doc_sha":"5c74f429970a2d2b","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"move(dx, dy) or (delta) -> None"},"name":{"doc_sha":"90a2ab0ec07c00eb","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"on_cell_click":{"doc_sha":"ada11ea920c71f29","kind":"property","lifecycle":{"since":"0.2.8-dev"},"signature":""},"on_cell_enter":{"doc_sha":"6ae7882d4f14f183","kind":"property","lifecycle":{"since":"0.2.8-dev"},"signature":""},"on_cell_exit":{"doc_sha":"729303b3a70b2a36","kind":"property","lifecycle":{"since":"0.2.8-dev"},"signature":""},"on_click":{"doc_sha":"4e33bd16b2dee20a","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"on_enter":{"doc_sha":"48c996802e262396","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"on_exit":{"doc_sha":"c14ef0d6d6d40b0e","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"on_move":{"doc_sha":"9c3e26818b5b25b0","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"opacity":{"doc_sha":"c1a7ef41e074519e","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"origin":{"doc_sha":"270bdf366c74bb1d","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"parent":{"doc_sha":"171b0de4e295c55b","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"perspective":{"doc_sha":"8854eef3677c4423","kind":"property","lifecycle":{"since":"0.2.8-dev"},"signature":""},"perspective_enabled":{"doc_sha":"abd2223cd551e1ca","kind":"property","lifecycle":{"since":"0.2.8-dev"},"signature":""},"pos":{"doc_sha":"7f556820c51df2c4","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"realign":{"doc_sha":"8dfa7333a49aa697","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"realign() -> None"},"resize":{"doc_sha":"005a04f1d129ebe5","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"resize(width, height) or (size) -> None"},"rotate_with_camera":{"doc_sha":"5c3af880218d3a08","kind":"property","lifecycle":{"modified":{"commit":"4bb1966","version":"0.2.8-dev"},"since":"0.2.8"},"signature":""},"rotation":{"doc_sha":"61ba3e66a0841a08","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"shader":{"doc_sha":"418952064319ffb5","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"size":{"doc_sha":"cfff2bccd482c2ef","kind":"property","lifecycle":{"since":"0.2.8-dev"},"signature":""},"texture":{"doc_sha":"b9bfb54e5571bfc9","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"uniforms":{"doc_sha":"b1f2699da618d002","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"vert_margin":{"doc_sha":"e2042be923bc6fcb","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"visible":{"doc_sha":"49221a4ae830f3b8","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"w":{"doc_sha":"ec55a5a578c4d993","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"x":{"doc_sha":"2449b0f04153753c","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"y":{"doc_sha":"7958c0625706f2d2","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"z_index":{"doc_sha":"0cb3df5278d2658b","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"zoom":{"doc_sha":"e56c4b61e0303f09","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""}}},"GridData":{"doc_sha":"793d86e4e51839f6","kind":"class","lifecycle":{"since":"0.2.8-dev"},"members":{"add_layer":{"doc_sha":"ad48373d656e6a88","kind":"method","lifecycle":{"since":"0.2.8-dev"},"signature":"add_layer(layer: ColorLayer | TileLayer) -> ColorLayer | TileLayer"},"apply_ranges":{"doc_sha":"f74ca906e65a4bec","kind":"method","lifecycle":{"since":"0.2.8-dev"},"signature":"apply_ranges(source: HeightMap, ranges: list) -> GridData"},"apply_threshold":{"doc_sha":"823c3779c6610389","kind":"method","lifecycle":{"since":"0.2.8-dev"},"signature":"apply_threshold(source: HeightMap, range: tuple, walkable: bool = None, transparent: bool = None) -> GridData"},"at":{"doc_sha":"0f12b1f70eaec14b","kind":"method","lifecycle":{"since":"0.2.8-dev"},"signature":"at(x: int, y: int) -> GridPoint"},"clear_dijkstra_maps":{"doc_sha":"d470811b59885b93","kind":"method","lifecycle":{"since":"0.2.8-dev"},"signature":"clear_dijkstra_maps() -> None"},"compute_fov":{"doc_sha":"2522197d48d43d98","kind":"method","lifecycle":{"since":"0.2.8-dev"},"signature":"compute_fov(pos, radius: int = 0, light_walls: bool = True, algorithm: FOV | int = FOV.BASIC) -> None"},"entities":{"doc_sha":"5c361e2150645749","kind":"property","lifecycle":{"since":"0.2.8-dev"},"signature":""},"entities_in_radius":{"doc_sha":"820857d7995c8fe6","kind":"method","lifecycle":{"since":"0.2.8-dev"},"signature":"entities_in_radius(pos: tuple | Vector, radius: float) -> list"},"find_path":{"doc_sha":"3de3f1a19ba24df8","kind":"method","lifecycle":{"since":"0.2.8-dev"},"signature":"find_path(start, end, diagonal_cost: float = 1.41, collide: str = None, heuristic = None, weight: float = 1.0) -> AStarPath | None"},"fov":{"doc_sha":"bb929647d575d246","kind":"property","lifecycle":{"since":"0.2.8-dev"},"signature":""},"fov_radius":{"doc_sha":"bf76a3199e6d7f0b","kind":"property","lifecycle":{"since":"0.2.8-dev"},"signature":""},"get_dijkstra_map":{"doc_sha":"af44def04f5c942e","kind":"method","lifecycle":{"since":"0.2.8-dev"},"signature":"get_dijkstra_map(root=None, diagonal_cost: float = 1.41, collide: str = None, roots=None) -> DijkstraMap"},"grid_h":{"doc_sha":"f96615d57a28b090","kind":"property","lifecycle":{"since":"0.2.8-dev"},"signature":""},"grid_size":{"doc_sha":"f1aff956f432c1fe","kind":"property","lifecycle":{"since":"0.2.8-dev"},"signature":""},"grid_w":{"doc_sha":"e2f7f55bad731ce6","kind":"property","lifecycle":{"since":"0.2.8-dev"},"signature":""},"is_in_fov":{"doc_sha":"3f871a007bbff04c","kind":"method","lifecycle":{"since":"0.2.8-dev"},"signature":"is_in_fov(x: int, y: int) -> bool"},"layer":{"doc_sha":"38d9c25b2b0b172b","kind":"method","lifecycle":{"since":"0.2.8-dev"},"signature":"layer(name: str) -> ColorLayer | TileLayer | None"},"layers":{"doc_sha":"9d6207bfc4909360","kind":"property","lifecycle":{"since":"0.2.8-dev"},"signature":""},"remove_layer":{"doc_sha":"7876426544d2ec0d","kind":"method","lifecycle":{"since":"0.2.8-dev"},"signature":"remove_layer(name_or_layer: str | ColorLayer | TileLayer) -> None"},"step":{"doc_sha":"38146d2fc84d70f3","kind":"method","lifecycle":{"since":"0.2.8-dev"},"signature":"step(n: int = 1, turn_order: int = None) -> None"},"texture":{"doc_sha":"a2911dd21b916bc0","kind":"property","lifecycle":{"since":"0.2.8-dev"},"signature":""}}},"GridView":{"doc_sha":"f88e43e992d43a86","kind":"class","lifecycle":{"since":"0.2.8"},"members":{"align":{"doc_sha":"58657c3cfa010f31","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"animate":{"doc_sha":"400df4f5893261c5","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"animate(property: str, target: Any, duration: float, easing=None, delta=False, loop=False, callback=None, conflict_mode='replace') -> Animation"},"bounds":{"doc_sha":"d3186ac3b3ff8bed","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"camera_rotation":{"doc_sha":"bc2cd502f52817e9","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"center":{"doc_sha":"913ef5c36eaf9635","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"center_camera":{"doc_sha":"00d37ccdedcd3a2e","kind":"method","lifecycle":{"since":"0.2.8-dev"},"signature":"center_camera(pos: tuple = None) -> None"},"center_x":{"doc_sha":"fbb0ff08116f3c80","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"center_y":{"doc_sha":"1bda27633025fb12","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"children":{"doc_sha":"9566d92d5619ee21","kind":"property","lifecycle":{"since":"0.2.8-dev"},"signature":""},"fill_color":{"doc_sha":"f26fb5da3de1789a","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"global_bounds":{"doc_sha":"aa4de9a20df55c2b","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"global_position":{"doc_sha":"b8dabbecc9fccd2a","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"grid_data":{"doc_sha":"fdf0084b8456fd45","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"h":{"doc_sha":"bd5a06618a50efad","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"horiz_margin":{"doc_sha":"0ef227a4cae6e675","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"hovered":{"doc_sha":"af0d353ce08f7be0","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"hovered_cell":{"doc_sha":"85c87c8aa013d0c4","kind":"property","lifecycle":{"since":"0.2.8-dev"},"signature":""},"margin":{"doc_sha":"ab774a7da8cfe9bf","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"move":{"doc_sha":"5c74f429970a2d2b","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"move(dx, dy) or (delta) -> None"},"name":{"doc_sha":"90a2ab0ec07c00eb","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"on_cell_click":{"doc_sha":"ada11ea920c71f29","kind":"property","lifecycle":{"since":"0.2.8-dev"},"signature":""},"on_cell_enter":{"doc_sha":"6ae7882d4f14f183","kind":"property","lifecycle":{"since":"0.2.8-dev"},"signature":""},"on_cell_exit":{"doc_sha":"729303b3a70b2a36","kind":"property","lifecycle":{"since":"0.2.8-dev"},"signature":""},"on_click":{"doc_sha":"4e33bd16b2dee20a","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"on_enter":{"doc_sha":"48c996802e262396","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"on_exit":{"doc_sha":"c14ef0d6d6d40b0e","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"on_move":{"doc_sha":"9c3e26818b5b25b0","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"opacity":{"doc_sha":"c1a7ef41e074519e","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"origin":{"doc_sha":"270bdf366c74bb1d","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"parent":{"doc_sha":"171b0de4e295c55b","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"perspective":{"doc_sha":"8854eef3677c4423","kind":"property","lifecycle":{"since":"0.2.8-dev"},"signature":""},"perspective_enabled":{"doc_sha":"abd2223cd551e1ca","kind":"property","lifecycle":{"since":"0.2.8-dev"},"signature":""},"pos":{"doc_sha":"7f556820c51df2c4","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"realign":{"doc_sha":"8dfa7333a49aa697","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"realign() -> None"},"resize":{"doc_sha":"005a04f1d129ebe5","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"resize(width, height) or (size) -> None"},"rotate_with_camera":{"doc_sha":"5c3af880218d3a08","kind":"property","lifecycle":{"modified":{"commit":"4bb1966","version":"0.2.8-dev"},"since":"0.2.8"},"signature":""},"rotation":{"doc_sha":"61ba3e66a0841a08","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"shader":{"doc_sha":"418952064319ffb5","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"size":{"doc_sha":"cfff2bccd482c2ef","kind":"property","lifecycle":{"since":"0.2.8-dev"},"signature":""},"texture":{"doc_sha":"b9bfb54e5571bfc9","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"uniforms":{"doc_sha":"b1f2699da618d002","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"vert_margin":{"doc_sha":"e2042be923bc6fcb","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"visible":{"doc_sha":"49221a4ae830f3b8","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"w":{"doc_sha":"ec55a5a578c4d993","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"x":{"doc_sha":"2449b0f04153753c","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"y":{"doc_sha":"7958c0625706f2d2","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"z_index":{"doc_sha":"0cb3df5278d2658b","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"zoom":{"doc_sha":"e56c4b61e0303f09","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""}}},"HeightMap":{"doc_sha":"e5f86ba9c89f2a5a","kind":"class","lifecycle":{"since":"0.2.8"},"members":{"add":{"doc_sha":"d420dd760d7228af","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"add(other: HeightMap, *, pos=None, source_pos=None, size=None) -> HeightMap"},"add_bsp":{"doc_sha":"b70f06e432c91a7a","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"add_bsp(bsp: BSP, *, pos=None, select: str = 'leaves', nodes: list = None, shrink: int = 0, value: float = 1.0) -> HeightMap"},"add_constant":{"doc_sha":"f382eb3f3e667cb4","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"add_constant(value: float, *, pos=None, size=None) -> HeightMap"},"add_hill":{"doc_sha":"1ad2406960701682","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"add_hill(center, radius: float, height: float) -> HeightMap"},"add_noise":{"doc_sha":"3c3c15026ff43959","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"add_noise(source: NoiseSource, world_origin: tuple = (0.0, 0.0), world_size: tuple = None, mode: str = 'fbm', octaves: int = 4, scale: float = 1.0) -> HeightMap"},"add_voronoi":{"doc_sha":"b689af51f6867b78","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"add_voronoi(num_points: int, coefficients: tuple = (1.0, -0.5), seed: int = None) -> HeightMap"},"clamp":{"doc_sha":"60c08680fb281402","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"clamp(min: float = 0.0, max: float = 1.0, *, pos=None, size=None) -> HeightMap"},"clear":{"doc_sha":"2297d98f231b6b7b","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"clear() -> HeightMap"},"copy_from":{"doc_sha":"80d64f3f6e76f8a7","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"copy_from(other: HeightMap, *, pos=None, source_pos=None, size=None) -> HeightMap"},"count_in_range":{"doc_sha":"7af3d60f28d614b6","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"count_in_range(range: tuple[float, float]) -> int"},"dig_bezier":{"doc_sha":"f3327d6ddc165496","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"dig_bezier(points: tuple, start_radius: float, end_radius: float, start_height: float, end_height: float) -> HeightMap"},"dig_hill":{"doc_sha":"be55e494c4e50c3a","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"dig_hill(center, radius: float, target_height: float) -> HeightMap"},"fill":{"doc_sha":"4e998a0b1d85c4f2","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"fill(value: float, *, pos=None, size=None) -> HeightMap"},"get":{"doc_sha":"f55fcc655d43dc50","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"get(x, y) or (pos) -> float"},"get_interpolated":{"doc_sha":"5c3e0267d9cbeda2","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"get_interpolated(x, y) or (pos) -> float"},"get_normal":{"doc_sha":"6d4212add4e533f2","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"get_normal(x, y, water_level=0.0) or (pos, water_level=0.0) -> tuple[float, float, float]"},"get_slope":{"doc_sha":"4876967f738c7d67","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"get_slope(x, y) or (pos) -> float"},"inverse":{"doc_sha":"f6c4de6cd6a9ca0f","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"inverse() -> HeightMap"},"lerp":{"doc_sha":"967cacc180df98bc","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"lerp(other: HeightMap, t: float, *, pos=None, source_pos=None, size=None) -> HeightMap"},"max":{"doc_sha":"141515f8b1f23390","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"max(other: HeightMap, *, pos=None, source_pos=None, size=None) -> HeightMap"},"mid_point_displacement":{"doc_sha":"bfcbfe796d73410f","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"mid_point_displacement(roughness: float = 0.5, seed: int = None) -> HeightMap"},"min":{"doc_sha":"c51e934ea5047d91","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"min(other: HeightMap, *, pos=None, source_pos=None, size=None) -> HeightMap"},"min_max":{"doc_sha":"4e0b3cb7472a7cfe","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"min_max() -> tuple[float, float]"},"multiply":{"doc_sha":"83d2fcea22463d18","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"multiply(other: HeightMap, *, pos=None, source_pos=None, size=None) -> HeightMap"},"multiply_bsp":{"doc_sha":"21f401c651344592","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"multiply_bsp(bsp: BSP, *, pos=None, select: str = 'leaves', nodes: list = None, shrink: int = 0, value: float = 1.0) -> HeightMap"},"multiply_noise":{"doc_sha":"177d1d06a401af54","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"multiply_noise(source: NoiseSource, world_origin: tuple = (0.0, 0.0), world_size: tuple = None, mode: str = 'fbm', octaves: int = 4, scale: float = 1.0) -> HeightMap"},"normalize":{"doc_sha":"93ce05bf80d158ed","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"normalize(min: float = 0.0, max: float = 1.0, *, pos=None, size=None) -> HeightMap"},"rain_erosion":{"doc_sha":"4dbe6bd42d9f6e5b","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"rain_erosion(drops: int, erosion: float = 0.1, sedimentation: float = 0.05, seed: int = None) -> HeightMap"},"scale":{"doc_sha":"c85d9324b84c43ff","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"scale(factor: float, *, pos=None, size=None) -> HeightMap"},"size":{"doc_sha":"31846e24f4028b35","kind":"property","lifecycle":{"since":"0.2.8"},"signature":"Dimensions (width, height) of the heightmap. Read-only."},"smooth":{"doc_sha":"243be30aa73da43b","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"smooth(iterations: int = 1) -> HeightMap"},"sparse_kernel":{"doc_sha":"e3b28d2c3913ac74","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"sparse_kernel(weights: dict[tuple[int, int], float]) -> HeightMap"},"sparse_kernel_from":{"doc_sha":"ebeed84050ee0b2f","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"sparse_kernel_from(source: HeightMap, weights: dict[tuple[int, int], float]) -> None"},"subtract":{"doc_sha":"8e209f74aaad794d","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"subtract(other: HeightMap, *, pos=None, source_pos=None, size=None) -> HeightMap"},"threshold":{"doc_sha":"946f6a1fb850decb","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"threshold(range: tuple[float, float]) -> HeightMap"},"threshold_binary":{"doc_sha":"6ebe7bd7cbc112c2","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"threshold_binary(range: tuple[float, float], value: float = 1.0) -> HeightMap"}}},"Heuristic":{"doc_sha":"7582db951ab67fc5","kind":"class","lifecycle":{"since":"0.2.8"},"members":{"as_integer_ratio":{"doc_sha":"3eb115fab0d0e0ec","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"($self, /)"},"bit_count":{"doc_sha":"f276da700b5a2e37","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"($self, /)"},"bit_length":{"doc_sha":"15b29db62fd0fa6c","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"($self, /)"},"conjugate":{"doc_sha":"6517a002faad7c75","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"($self, /)"},"denominator":{"doc_sha":"5051eb1570768851","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"from_bytes":{"doc_sha":"64672607865643a7","kind":"classmethod","lifecycle":{"since":"0.2.8"},"signature":"($type, /, bytes, byteorder='big', *, signed=False)"},"imag":{"doc_sha":"ef78ea410df39b26","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"is_integer":{"doc_sha":"6176763fbac2fd22","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"($self, /)"},"numerator":{"doc_sha":"ed255748ce2eda7c","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"real":{"doc_sha":"d44497f1990af6fe","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"to_bytes":{"doc_sha":"b452319e48e64764","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"($self, /, length=1, byteorder='big', *, signed=False)"}}},"InputState":{"doc_sha":"c7b7558edda29fea","kind":"class","lifecycle":{"since":"0.2.8"},"members":{"as_integer_ratio":{"doc_sha":"3eb115fab0d0e0ec","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"($self, /)"},"bit_count":{"doc_sha":"f276da700b5a2e37","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"($self, /)"},"bit_length":{"doc_sha":"15b29db62fd0fa6c","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"($self, /)"},"conjugate":{"doc_sha":"6517a002faad7c75","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"($self, /)"},"denominator":{"doc_sha":"5051eb1570768851","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"from_bytes":{"doc_sha":"64672607865643a7","kind":"classmethod","lifecycle":{"since":"0.2.8"},"signature":"($type, /, bytes, byteorder='big', *, signed=False)"},"imag":{"doc_sha":"ef78ea410df39b26","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"is_integer":{"doc_sha":"6176763fbac2fd22","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"($self, /)"},"numerator":{"doc_sha":"ed255748ce2eda7c","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"real":{"doc_sha":"d44497f1990af6fe","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"to_bytes":{"doc_sha":"b452319e48e64764","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"($self, /, length=1, byteorder='big', *, signed=False)"}}},"Key":{"doc_sha":"bc4feb60367e4be7","kind":"class","lifecycle":{"since":"0.2.8"},"members":{"as_integer_ratio":{"doc_sha":"3eb115fab0d0e0ec","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"($self, /)"},"bit_count":{"doc_sha":"f276da700b5a2e37","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"($self, /)"},"bit_length":{"doc_sha":"15b29db62fd0fa6c","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"($self, /)"},"conjugate":{"doc_sha":"6517a002faad7c75","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"($self, /)"},"denominator":{"doc_sha":"5051eb1570768851","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"from_bytes":{"doc_sha":"64672607865643a7","kind":"classmethod","lifecycle":{"since":"0.2.8"},"signature":"($type, /, bytes, byteorder='big', *, signed=False)"},"imag":{"doc_sha":"ef78ea410df39b26","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"is_integer":{"doc_sha":"6176763fbac2fd22","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"($self, /)"},"numerator":{"doc_sha":"ed255748ce2eda7c","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"real":{"doc_sha":"d44497f1990af6fe","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"to_bytes":{"doc_sha":"b452319e48e64764","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"($self, /, length=1, byteorder='big', *, signed=False)"}}},"Keyboard":{"doc_sha":"afbde6bcf7bfe1c6","kind":"class","lifecycle":{"since":"0.2.8"},"members":{"alt":{"doc_sha":"051cb7dbc470d328","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"ctrl":{"doc_sha":"c062912b0b816bd2","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"shift":{"doc_sha":"614beb0d432c2ec0","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"system":{"doc_sha":"2292efb47472f5e7","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""}}},"LdtkProject":{"doc_sha":"d80a5fdda811cb33","kind":"class","lifecycle":{"since":"0.2.8"},"members":{"enums":{"doc_sha":"5697f9e75980e2be","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"level":{"doc_sha":"33b80035be788d03","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"level(name: str) -> dict"},"level_names":{"doc_sha":"9479f6c2bd85608d","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"ruleset":{"doc_sha":"ce72d154e5dd391c","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"ruleset(name: str) -> AutoRuleSet"},"ruleset_names":{"doc_sha":"28983e9702e0dc81","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"tileset":{"doc_sha":"30eabe3003acb244","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"tileset(name: str) -> TileSetFile"},"tileset_names":{"doc_sha":"77a45b51f49743ef","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"version":{"doc_sha":"e6d3d49ad9edfad8","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""}}},"Line":{"doc_sha":"8d778faa93b5d0d5","kind":"class","lifecycle":{"since":"0.2.8"},"members":{"align":{"doc_sha":"58657c3cfa010f31","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"animate":{"doc_sha":"400df4f5893261c5","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"animate(property: str, target: Any, duration: float, easing=None, delta=False, loop=False, callback=None, conflict_mode='replace') -> Animation"},"bounds":{"doc_sha":"d3186ac3b3ff8bed","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"color":{"doc_sha":"f7aca0b82eaae97e","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"end":{"doc_sha":"cc14ff1baa1b61fa","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"global_bounds":{"doc_sha":"aa4de9a20df55c2b","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"global_position":{"doc_sha":"b8dabbecc9fccd2a","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"grid_pos":{"doc_sha":"9df77e9a996208ba","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"grid_size":{"doc_sha":"e32208817299dfe9","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"horiz_margin":{"doc_sha":"0ef227a4cae6e675","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"hovered":{"doc_sha":"af0d353ce08f7be0","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"margin":{"doc_sha":"ab774a7da8cfe9bf","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"move":{"doc_sha":"5c74f429970a2d2b","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"move(dx, dy) or (delta) -> None"},"name":{"doc_sha":"feeb7cb507a4692d","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"on_click":{"doc_sha":"49cecfb458b010a1","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"on_enter":{"doc_sha":"48c996802e262396","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"on_exit":{"doc_sha":"c14ef0d6d6d40b0e","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"on_move":{"doc_sha":"9c3e26818b5b25b0","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"opacity":{"doc_sha":"c1a7ef41e074519e","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"origin":{"doc_sha":"270bdf366c74bb1d","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"parent":{"doc_sha":"171b0de4e295c55b","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"pos":{"doc_sha":"733a48ea3a730e60","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"realign":{"doc_sha":"8dfa7333a49aa697","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"realign() -> None"},"resize":{"doc_sha":"005a04f1d129ebe5","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"resize(width, height) or (size) -> None"},"rotate_with_camera":{"doc_sha":"5c3af880218d3a08","kind":"property","lifecycle":{"modified":{"commit":"4bb1966","version":"0.2.8-dev"},"since":"0.2.8"},"signature":""},"rotation":{"doc_sha":"61ba3e66a0841a08","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"start":{"doc_sha":"817049419558ea17","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"thickness":{"doc_sha":"b6c7f3e979028709","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"vert_margin":{"doc_sha":"e2042be923bc6fcb","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"visible":{"doc_sha":"49221a4ae830f3b8","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"z_index":{"doc_sha":"cecb3704206d35f7","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""}}},"Model3D":{"doc_sha":"be7fb5003ed48d84","kind":"class","lifecycle":{"since":"0.2.8"},"members":{"animation_clips":{"doc_sha":"0d15ad9839919fe5","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"bone_count":{"doc_sha":"6e389fb2496946bf","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"bounds":{"doc_sha":"4ddfb8f6ae5a8928","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"cube":{"doc_sha":"b49160efa8b0f71e","kind":"classmethod","lifecycle":{"since":"0.2.8"},"signature":"cube(size=1.0) -> Model3D"},"has_skeleton":{"doc_sha":"4e9ac073dac101b6","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"mesh_count":{"doc_sha":"49f8fe0ece668d77","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"name":{"doc_sha":"b577d22f9a18de0c","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"plane":{"doc_sha":"5e31b94e295b4429","kind":"classmethod","lifecycle":{"since":"0.2.8"},"signature":"plane(width=1.0, depth=1.0, segments=1) -> Model3D"},"sphere":{"doc_sha":"b385d8aaf0efd485","kind":"classmethod","lifecycle":{"since":"0.2.8"},"signature":"sphere(radius=0.5, segments=16, rings=12) -> Model3D"},"triangle_count":{"doc_sha":"b67c98fac8ffc8dd","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"vertex_count":{"doc_sha":"8b76e70b26a820df","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""}}},"Mouse":{"doc_sha":"e64f561c8a08bdec","kind":"class","lifecycle":{"since":"0.2.8"},"members":{"grabbed":{"doc_sha":"3da420bb9cac434f","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"left":{"doc_sha":"a4c7b98babc093cb","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"middle":{"doc_sha":"1306b1240c6f64e3","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"pos":{"doc_sha":"2ecba0a68d64976a","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"right":{"doc_sha":"08d5534c1c034b39","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"visible":{"doc_sha":"834527eb5579f3e3","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"x":{"doc_sha":"c142f98ee94bf3f7","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"y":{"doc_sha":"5095db9e9e163e92","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""}}},"MouseButton":{"doc_sha":"a5a7584949d18f01","kind":"class","lifecycle":{"since":"0.2.8"},"members":{"as_integer_ratio":{"doc_sha":"3eb115fab0d0e0ec","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"($self, /)"},"bit_count":{"doc_sha":"f276da700b5a2e37","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"($self, /)"},"bit_length":{"doc_sha":"15b29db62fd0fa6c","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"($self, /)"},"conjugate":{"doc_sha":"6517a002faad7c75","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"($self, /)"},"denominator":{"doc_sha":"5051eb1570768851","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"from_bytes":{"doc_sha":"64672607865643a7","kind":"classmethod","lifecycle":{"since":"0.2.8"},"signature":"($type, /, bytes, byteorder='big', *, signed=False)"},"imag":{"doc_sha":"ef78ea410df39b26","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"is_integer":{"doc_sha":"6176763fbac2fd22","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"($self, /)"},"numerator":{"doc_sha":"ed255748ce2eda7c","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"real":{"doc_sha":"d44497f1990af6fe","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"to_bytes":{"doc_sha":"b452319e48e64764","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"($self, /, length=1, byteorder='big', *, signed=False)"}}},"Music":{"doc_sha":"e13e6ad378ec0e91","kind":"class","lifecycle":{"since":"0.2.8"},"members":{"duration":{"doc_sha":"25607c5fa852d501","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"loop":{"doc_sha":"a165a91f8ff771f5","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"pause":{"doc_sha":"4eb6d242151147dc","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"pause() -> None"},"play":{"doc_sha":"f724039e0cdb2847","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"play() -> None"},"playing":{"doc_sha":"4451d007747fa3b1","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"position":{"doc_sha":"608a5f6d4ed927bc","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"source":{"doc_sha":"ee5d7818bae0a255","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"stop":{"doc_sha":"f3289bbb98a4da1f","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"stop() -> None"},"volume":{"doc_sha":"749fc9d89252cb0e","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""}}},"NoiseSource":{"doc_sha":"3bfa3b53984c85cb","kind":"class","lifecycle":{"since":"0.2.8"},"members":{"algorithm":{"doc_sha":"ba813d6c022f15f0","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"dimensions":{"doc_sha":"f534d20928d8d67a","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"fbm":{"doc_sha":"d13c8670fc68b9df","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"fbm(pos: tuple[float, ...], octaves: int = 4) -> float"},"get":{"doc_sha":"3d7776fffd7a0a39","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"get(pos: tuple[float, ...]) -> float"},"hurst":{"doc_sha":"11682e6dc42f2417","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"lacunarity":{"doc_sha":"4d07f7079f3938de","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"sample":{"doc_sha":"63fd0e680000d0f0","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"sample(size: tuple[int, int], world_origin: tuple[float, float] = (0.0, 0.0), world_size: tuple[float, float] = None, mode: str = 'fbm', octaves: int = 4) -> HeightMap"},"seed":{"doc_sha":"17bc1e90a8ce6b23","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"turbulence":{"doc_sha":"800c841f913b3780","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"turbulence(pos: tuple[float, ...], octaves: int = 4) -> float"}}},"Perspective":{"doc_sha":"e1c8553ea2a32a40","kind":"class","lifecycle":{"since":"0.2.8"},"members":{"as_integer_ratio":{"doc_sha":"3eb115fab0d0e0ec","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"($self, /)"},"bit_count":{"doc_sha":"f276da700b5a2e37","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"($self, /)"},"bit_length":{"doc_sha":"15b29db62fd0fa6c","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"($self, /)"},"conjugate":{"doc_sha":"6517a002faad7c75","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"($self, /)"},"denominator":{"doc_sha":"5051eb1570768851","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"from_bytes":{"doc_sha":"64672607865643a7","kind":"classmethod","lifecycle":{"since":"0.2.8"},"signature":"($type, /, bytes, byteorder='big', *, signed=False)"},"imag":{"doc_sha":"ef78ea410df39b26","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"is_integer":{"doc_sha":"6176763fbac2fd22","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"($self, /)"},"numerator":{"doc_sha":"ed255748ce2eda7c","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"real":{"doc_sha":"d44497f1990af6fe","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"to_bytes":{"doc_sha":"b452319e48e64764","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"($self, /, length=1, byteorder='big', *, signed=False)"}}},"PropertyBinding":{"doc_sha":"a23eb43edd46bb66","kind":"class","lifecycle":{"since":"0.2.8"},"members":{"is_valid":{"doc_sha":"40ada14485c362a7","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"property":{"doc_sha":"0dad06f4e661db2d","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"target":{"doc_sha":"3b15e1922fd910c9","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"value":{"doc_sha":"80d28b4e329837d8","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""}}},"Scene":{"doc_sha":"a156c42d77b20a3e","kind":"class","lifecycle":{"since":"0.2.8"},"members":{"activate":{"doc_sha":"54ae3cd3cf652660","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"activate(transition: Transition = None, duration: float = None) -> None"},"active":{"doc_sha":"a59f70d3c12a7cfe","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"children":{"doc_sha":"f21fb480dac856b6","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"name":{"doc_sha":"7feed093cdf9334a","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"on_key":{"doc_sha":"40ece618402a45f3","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"opacity":{"doc_sha":"9e60a4ae9bdb493b","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"pos":{"doc_sha":"50e29b0dd34a76d5","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"realign":{"doc_sha":"eef1e2dce9ee8894","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"realign() -> None"},"register":{"doc_sha":"7603337801a7a498","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"register() -> None"},"registered":{"doc_sha":"d9ff54f488ed5b87","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"unregister":{"doc_sha":"fda6be9891d1f502","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"unregister() -> None"},"visible":{"doc_sha":"a7f321f4013dfb9b","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""}}},"Shader":{"doc_sha":"aded56cb37cba9d8","kind":"class","lifecycle":{"since":"0.2.8"},"members":{"dynamic":{"doc_sha":"43d8bc587c3997e3","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"is_valid":{"doc_sha":"fcdf95eedfa8479e","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"set_uniform":{"doc_sha":"bdaf877e5bc28f21","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"set_uniform(name: str, value: float|tuple) -> None"},"source":{"doc_sha":"dbd462698aadf450","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""}}},"Sound":{"doc_sha":"8f31cca1781580d5","kind":"class","lifecycle":{"since":"0.2.8"},"members":{"buffer":{"doc_sha":"c79f4e433d4300ac","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"duration":{"doc_sha":"48daac900b6eaf36","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"loop":{"doc_sha":"a624dff0e72a5337","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"pause":{"doc_sha":"005627aca21692d3","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"pause() -> None"},"pitch":{"doc_sha":"80b38bfc202b4291","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"play":{"doc_sha":"ae090ee8c0320797","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"play() -> None"},"play_varied":{"doc_sha":"fd9b5cd6db3cb1cf","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"play_varied(pitch_range: float = 0.1, volume_range: float = 3.0) -> None"},"playing":{"doc_sha":"8a0c491fa38061d5","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"source":{"doc_sha":"1736ca7393d96c48","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"stop":{"doc_sha":"f3289bbb98a4da1f","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"stop() -> None"},"volume":{"doc_sha":"749fc9d89252cb0e","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""}}},"SoundBuffer":{"doc_sha":"426618cde68541be","kind":"class","lifecycle":{"since":"0.2.8"},"members":{"bit_crush":{"doc_sha":"66ae55272fd9d182","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"bit_crush(bits: int, rate_divisor: int) -> SoundBuffer"},"channels":{"doc_sha":"f3948c1ceb5d7aec","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"concat":{"doc_sha":"7ee10bfe76aae5e2","kind":"classmethod","lifecycle":{"since":"0.2.8"},"signature":"concat(buffers: list[SoundBuffer], overlap: float = 0.0) -> SoundBuffer"},"distortion":{"doc_sha":"a36442ca725e5cc6","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"distortion(drive: float) -> SoundBuffer"},"duration":{"doc_sha":"bc1c03f781e0372b","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"echo":{"doc_sha":"1b93f79c671068ab","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"echo(delay_ms: float, feedback: float, wet: float) -> SoundBuffer"},"from_samples":{"doc_sha":"556326f1f2cda4a7","kind":"classmethod","lifecycle":{"since":"0.2.8"},"signature":"from_samples(data: bytes, channels: int, sample_rate: int) -> SoundBuffer"},"gain":{"doc_sha":"22fc3aad26b084a5","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"gain(factor: float) -> SoundBuffer"},"high_pass":{"doc_sha":"7d8af12720a4bbae","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"high_pass(cutoff_hz: float) -> SoundBuffer"},"low_pass":{"doc_sha":"82cdc215e7da9310","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"low_pass(cutoff_hz: float) -> SoundBuffer"},"mix":{"doc_sha":"4df14cf741427da9","kind":"classmethod","lifecycle":{"since":"0.2.8"},"signature":"mix(buffers: list[SoundBuffer]) -> SoundBuffer"},"normalize":{"doc_sha":"5476b16208b1ac7b","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"normalize() -> SoundBuffer"},"pitch_shift":{"doc_sha":"ee9373f2d71fdaa8","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"pitch_shift(factor: float) -> SoundBuffer"},"reverb":{"doc_sha":"b6d0dcf1e5f75a42","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"reverb(room_size: float, damping: float, wet: float) -> SoundBuffer"},"reverse":{"doc_sha":"eb70bfa5da4c860d","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"reverse() -> SoundBuffer"},"sample_count":{"doc_sha":"5e96b7263eade6af","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"sample_rate":{"doc_sha":"a41417a71e10a2d6","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"sfxr":{"doc_sha":"5f45a4b7afc74066","kind":"classmethod","lifecycle":{"since":"0.2.8"},"signature":"sfxr(preset: str = None, seed: int = None, **params) -> SoundBuffer"},"sfxr_mutate":{"doc_sha":"68b5b653de4f55dc","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"sfxr_mutate(amount: float = 0.05, seed: int = None) -> SoundBuffer"},"sfxr_params":{"doc_sha":"7fd7a2356600014a","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"slice":{"doc_sha":"84099d0f46e6b7ad","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"slice(start: float, end: float) -> SoundBuffer"},"tone":{"doc_sha":"eaec1748a9089774","kind":"classmethod","lifecycle":{"since":"0.2.8"},"signature":"tone(frequency: float, duration: float, waveform: str = 'sine', ...) -> SoundBuffer"}}},"Sprite":{"doc_sha":"a687c4f393725428","kind":"class","lifecycle":{"since":"0.2.8"},"members":{"align":{"doc_sha":"58657c3cfa010f31","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"animate":{"doc_sha":"400df4f5893261c5","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"animate(property: str, target: Any, duration: float, easing=None, delta=False, loop=False, callback=None, conflict_mode='replace') -> Animation"},"bounds":{"doc_sha":"d3186ac3b3ff8bed","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"global_bounds":{"doc_sha":"aa4de9a20df55c2b","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"global_position":{"doc_sha":"b8dabbecc9fccd2a","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"grid_pos":{"doc_sha":"9df77e9a996208ba","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"grid_size":{"doc_sha":"e32208817299dfe9","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"horiz_margin":{"doc_sha":"0ef227a4cae6e675","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"hovered":{"doc_sha":"af0d353ce08f7be0","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"margin":{"doc_sha":"ab774a7da8cfe9bf","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"move":{"doc_sha":"5c74f429970a2d2b","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"move(dx, dy) or (delta) -> None"},"name":{"doc_sha":"90a2ab0ec07c00eb","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"on_click":{"doc_sha":"b3b8de4e294d71b6","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"on_enter":{"doc_sha":"48c996802e262396","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"on_exit":{"doc_sha":"c14ef0d6d6d40b0e","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"on_move":{"doc_sha":"9c3e26818b5b25b0","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"opacity":{"doc_sha":"c1a7ef41e074519e","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"origin":{"doc_sha":"270bdf366c74bb1d","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"parent":{"doc_sha":"171b0de4e295c55b","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"pos":{"doc_sha":"55a118e7889fd555","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"realign":{"doc_sha":"8dfa7333a49aa697","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"realign() -> None"},"resize":{"doc_sha":"005a04f1d129ebe5","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"resize(width, height) or (size) -> None"},"rotate_with_camera":{"doc_sha":"5c3af880218d3a08","kind":"property","lifecycle":{"modified":{"commit":"4bb1966","version":"0.2.8-dev"},"since":"0.2.8"},"signature":""},"rotation":{"doc_sha":"61ba3e66a0841a08","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"scale":{"doc_sha":"f91a66d322ee1a75","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"scale_x":{"doc_sha":"95f47a809a8ac927","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"scale_y":{"doc_sha":"8567691c22a9b5f8","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"shader":{"doc_sha":"418952064319ffb5","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"sprite_index":{"doc_sha":"826af9fc52ae5cef","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"texture":{"doc_sha":"df3e7671926aa0a3","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"uniforms":{"doc_sha":"b1f2699da618d002","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"vert_margin":{"doc_sha":"e2042be923bc6fcb","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"visible":{"doc_sha":"49221a4ae830f3b8","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"x":{"doc_sha":"528487f8deaaf789","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"y":{"doc_sha":"114ecec940477107","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"z_index":{"doc_sha":"328d87ae44d9fb71","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""}}},"Texture":{"doc_sha":"da368b536d9f844e","kind":"class","lifecycle":{"since":"0.2.8"},"members":{"composite":{"doc_sha":"0bf406582f618af3","kind":"classmethod","lifecycle":{"since":"0.2.8"},"signature":"composite(layers: list[Texture], sprite_width: int, sprite_height: int, name: str = '') -> Texture"},"display_height":{"doc_sha":"0dc1ccea45ff4e40","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"display_offset_x":{"doc_sha":"0ca738fa6559aabb","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"display_offset_y":{"doc_sha":"83b5bfadd26f4e79","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"display_width":{"doc_sha":"cfc6525a8a0b5bb7","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"from_bytes":{"doc_sha":"979f5a384770d87f","kind":"classmethod","lifecycle":{"since":"0.2.8"},"signature":"from_bytes(data: bytes, width: int, height: int, sprite_width: int, sprite_height: int, name: str = '') -> Texture"},"hsl_shift":{"doc_sha":"95f19de7a67646e3","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"hsl_shift(hue_shift: float, sat_shift: float = 0.0, lit_shift: float = 0.0) -> Texture"},"sheet_height":{"doc_sha":"59d4fb71cae72fba","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"sheet_width":{"doc_sha":"54ef54eb9c39a907","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"source":{"doc_sha":"51bee1cdb17257ba","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"sprite_count":{"doc_sha":"d1ed79d8b59eca99","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"sprite_height":{"doc_sha":"19b6a36f45f965e8","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"sprite_width":{"doc_sha":"6f9276522c3b2512","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""}}},"TileLayer":{"doc_sha":"5551e3bf9e668d89","kind":"class","lifecycle":{"since":"0.2.8"},"members":{"apply_ranges":{"doc_sha":"e443e268532fdd2f","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"apply_ranges(source: HeightMap, ranges: list) -> TileLayer"},"apply_threshold":{"doc_sha":"2253f4367656d073","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"apply_threshold(source: HeightMap, range: tuple, tile: int) -> TileLayer"},"at":{"doc_sha":"19975fea19b3fadb","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"at(pos: tuple | Vector) or (x: int, y: int) -> int"},"edit":{"doc_sha":"47c32fda0e88780a","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"edit() -> context manager"},"fill":{"doc_sha":"5c8eca820794a605","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"fill(index: int) -> None"},"fill_rect":{"doc_sha":"adfc5ff6d6e91cc8","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"fill_rect(pos: tuple, size: tuple, index: int) -> None"},"grid":{"doc_sha":"b965c55d57de78fa","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"grid_size":{"doc_sha":"eac88eb0f7b16cbf","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"name":{"doc_sha":"dd282cc9273e2e36","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"set":{"doc_sha":"2b242556ce84080f","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"set(pos, index: int) -> None"},"texture":{"doc_sha":"1d211a219f88c72c","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"visible":{"doc_sha":"ecea81ec16e31c19","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"z_index":{"doc_sha":"eb13c1b4de11ce63","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""}}},"TileMapFile":{"doc_sha":"b26cb3870e41a2c9","kind":"class","lifecycle":{"since":"0.2.8"},"members":{"apply_to_tile_layer":{"doc_sha":"436e4fa972bd79d0","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"apply_to_tile_layer(tile_layer: TileLayer, layer_name: str, tileset_index: int = 0) -> None"},"height":{"doc_sha":"72aa81aefdab3ddb","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"object_layer":{"doc_sha":"e658613bcbbe9b3a","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"object_layer(name: str) -> list[dict]"},"object_layer_names":{"doc_sha":"7af1cc1454cd97f9","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"orientation":{"doc_sha":"2ee759a156b5c903","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"properties":{"doc_sha":"f2cb52eb132891d8","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"resolve_gid":{"doc_sha":"4a72f1c9f61cec27","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"resolve_gid(gid: int) -> tuple[int, int]"},"tile_height":{"doc_sha":"cc47760c48b8c9be","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"tile_layer_data":{"doc_sha":"399fd655e6402442","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"tile_layer_data(name: str) -> list[int]"},"tile_layer_names":{"doc_sha":"e225e2d4b71155e7","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"tile_width":{"doc_sha":"c109808c1e2d3ca1","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"tileset":{"doc_sha":"a3e35f368c3cc89d","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"tileset(index: int) -> tuple[int, TileSetFile]"},"tileset_count":{"doc_sha":"e7347387a4bcfeae","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"width":{"doc_sha":"acef1b7138a85569","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""}}},"TileSetFile":{"doc_sha":"ec83598f02a06258","kind":"class","lifecycle":{"since":"0.2.8"},"members":{"columns":{"doc_sha":"5ba187696ce9596e","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"image_source":{"doc_sha":"f21faacc0392eec3","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"margin":{"doc_sha":"6e1b3cfd5b59af6a","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"name":{"doc_sha":"5e0a76b40f4db151","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"properties":{"doc_sha":"e0382abd62d297a6","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"spacing":{"doc_sha":"950416fc0b8f1837","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"tile_count":{"doc_sha":"df5ccd0141400827","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"tile_height":{"doc_sha":"71a327fd061c1cf2","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"tile_info":{"doc_sha":"b33077f44c79e050","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"tile_info(tile_id: int) -> dict | None"},"tile_width":{"doc_sha":"888787b1ef69f5d9","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"to_texture":{"doc_sha":"610adac758051370","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"to_texture() -> Texture"},"wang_set":{"doc_sha":"c3b15db4611c250a","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"wang_set(name: str) -> WangSet"},"wang_sets":{"doc_sha":"71541a1c5777b29f","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""}}},"Timer":{"doc_sha":"646feff7bdb817f5","kind":"class","lifecycle":{"since":"0.2.8"},"members":{"active":{"doc_sha":"0ac0f4a607819264","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"callback":{"doc_sha":"a943e7e99b75fa22","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"interval":{"doc_sha":"e32658ee97f94823","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"name":{"doc_sha":"98a62482b59640f6","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"once":{"doc_sha":"7d5a7d20bae3793e","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"pause":{"doc_sha":"bfe544cc3c9f1cda","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"pause() -> None"},"paused":{"doc_sha":"060df10cbfc34662","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"remaining":{"doc_sha":"e223a64ed6a1ec63","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"restart":{"doc_sha":"a8c305ae5017cce0","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"restart() -> None"},"resume":{"doc_sha":"66ab0cd39f579ef9","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"resume() -> None"},"start":{"doc_sha":"0b2b51d812995d34","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"start() -> None"},"stop":{"doc_sha":"9f69f5e68512116c","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"stop() -> None"},"stopped":{"doc_sha":"bffdb8aa4589c28b","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""}}},"Transition":{"doc_sha":"e3b0c44298fc1c14","kind":"class","lifecycle":{"since":"0.2.8"},"members":{"as_integer_ratio":{"doc_sha":"3eb115fab0d0e0ec","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"($self, /)"},"bit_count":{"doc_sha":"f276da700b5a2e37","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"($self, /)"},"bit_length":{"doc_sha":"15b29db62fd0fa6c","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"($self, /)"},"conjugate":{"doc_sha":"6517a002faad7c75","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"($self, /)"},"denominator":{"doc_sha":"5051eb1570768851","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"from_bytes":{"doc_sha":"64672607865643a7","kind":"classmethod","lifecycle":{"since":"0.2.8"},"signature":"($type, /, bytes, byteorder='big', *, signed=False)"},"imag":{"doc_sha":"ef78ea410df39b26","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"is_integer":{"doc_sha":"6176763fbac2fd22","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"($self, /)"},"numerator":{"doc_sha":"ed255748ce2eda7c","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"real":{"doc_sha":"d44497f1990af6fe","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"to_bytes":{"doc_sha":"b452319e48e64764","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"($self, /, length=1, byteorder='big', *, signed=False)"}}},"Traversal":{"doc_sha":"e3b0c44298fc1c14","kind":"class","lifecycle":{"since":"0.2.8"},"members":{"as_integer_ratio":{"doc_sha":"3eb115fab0d0e0ec","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"($self, /)"},"bit_count":{"doc_sha":"f276da700b5a2e37","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"($self, /)"},"bit_length":{"doc_sha":"15b29db62fd0fa6c","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"($self, /)"},"conjugate":{"doc_sha":"6517a002faad7c75","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"($self, /)"},"denominator":{"doc_sha":"5051eb1570768851","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"from_bytes":{"doc_sha":"64672607865643a7","kind":"classmethod","lifecycle":{"since":"0.2.8"},"signature":"($type, /, bytes, byteorder='big', *, signed=False)"},"imag":{"doc_sha":"ef78ea410df39b26","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"is_integer":{"doc_sha":"6176763fbac2fd22","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"($self, /)"},"numerator":{"doc_sha":"ed255748ce2eda7c","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"real":{"doc_sha":"d44497f1990af6fe","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"to_bytes":{"doc_sha":"b452319e48e64764","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"($self, /, length=1, byteorder='big', *, signed=False)"}}},"Trigger":{"doc_sha":"3f698ad8b187be9f","kind":"class","lifecycle":{"since":"0.2.8"},"members":{"as_integer_ratio":{"doc_sha":"3eb115fab0d0e0ec","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"($self, /)"},"bit_count":{"doc_sha":"f276da700b5a2e37","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"($self, /)"},"bit_length":{"doc_sha":"15b29db62fd0fa6c","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"($self, /)"},"conjugate":{"doc_sha":"6517a002faad7c75","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"($self, /)"},"denominator":{"doc_sha":"5051eb1570768851","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"from_bytes":{"doc_sha":"64672607865643a7","kind":"classmethod","lifecycle":{"since":"0.2.8"},"signature":"($type, /, bytes, byteorder='big', *, signed=False)"},"imag":{"doc_sha":"ef78ea410df39b26","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"is_integer":{"doc_sha":"6176763fbac2fd22","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"($self, /)"},"numerator":{"doc_sha":"ed255748ce2eda7c","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"real":{"doc_sha":"d44497f1990af6fe","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"to_bytes":{"doc_sha":"b452319e48e64764","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"($self, /, length=1, byteorder='big', *, signed=False)"}}},"Vector":{"doc_sha":"0c8ae9e2ef387065","kind":"class","lifecycle":{"since":"0.2.8"},"members":{"angle":{"doc_sha":"a0ca5f1f4a89d780","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"angle() -> float"},"copy":{"doc_sha":"120e723fd513e072","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"copy() -> Vector"},"distance_to":{"doc_sha":"4f5d26bbe8434f65","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"distance_to(other: Vector) -> float"},"dot":{"doc_sha":"d69442ea289adfb8","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"dot(other: Vector) -> float"},"floor":{"doc_sha":"c7e81a90c5420968","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"floor() -> Vector"},"int":{"doc_sha":"3d97cae0ad8a963c","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"magnitude":{"doc_sha":"75f61a474afe3121","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"magnitude() -> float"},"magnitude_squared":{"doc_sha":"c976db5b540f827b","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"magnitude_squared() -> float"},"normalize":{"doc_sha":"04b468da3fc60858","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"normalize() -> Vector"},"x":{"doc_sha":"0e322bdd857e652b","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"y":{"doc_sha":"bbac84f15b6740d9","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""}}},"Viewport3D":{"doc_sha":"c41e58effff86261","kind":"class","lifecycle":{"since":"0.2.8"},"members":{"add_billboard":{"doc_sha":"ae2357f78a17a437","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"add_billboard(billboard)"},"add_layer":{"doc_sha":"b4c0274f9217aaa1","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"add_layer(name, z_index=0) -> dict"},"add_mesh":{"doc_sha":"588dbdf9a48f17e5","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"add_mesh(layer_name, model, pos, rotation=0, scale=1.0) -> int"},"add_voxel_layer":{"doc_sha":"47f17324425647e1","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"add_voxel_layer(voxel_grid, z_index=0)"},"animate":{"doc_sha":"400df4f5893261c5","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"animate(property: str, target: Any, duration: float, easing=None, delta=False, loop=False, callback=None, conflict_mode='replace') -> Animation"},"apply_heightmap":{"doc_sha":"be8e92a25726238d","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"apply_heightmap(heightmap, y_scale=1.0)"},"apply_terrain_colors":{"doc_sha":"cf0e47c185484e70","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"apply_terrain_colors(layer_name, r_map, g_map, b_map)"},"apply_threshold":{"doc_sha":"3adda69e435613d7","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"apply_threshold(heightmap, min_height, max_height, walkable=True)"},"at":{"doc_sha":"7ec76d7602627cb9","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"at(x, z) -> VoxelPoint"},"bg_color":{"doc_sha":"c7e7141bd1cadaea","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"billboard_count":{"doc_sha":"4b432c5c560ee44f","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"billboard_count() -> int"},"bounds":{"doc_sha":"d3186ac3b3ff8bed","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"build_terrain":{"doc_sha":"5be2a61f78907326","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"build_terrain(layer_name, heightmap, y_scale=1.0, cell_size=1.0) -> int"},"camera_pos":{"doc_sha":"7bfead8afc2b8312","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"camera_target":{"doc_sha":"3ecf7bb4dbdf27e4","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"cell_size":{"doc_sha":"8c3fa657c8fdc2aa","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"clear_billboards":{"doc_sha":"63d6d64c39b46264","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"clear_billboards()"},"clear_meshes":{"doc_sha":"1d6eb71f274020cf","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"clear_meshes(layer_name)"},"clear_voxel_nav_region":{"doc_sha":"56901862e4dc595a","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"clear_voxel_nav_region(voxel_grid)"},"compute_fov":{"doc_sha":"93c68ed57619564e","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"compute_fov(origin, radius=10) -> list"},"enable_affine":{"doc_sha":"96ba76ed43593940","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"enable_dither":{"doc_sha":"da6733dd08751b72","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"enable_fog":{"doc_sha":"ca6c5ffcef454868","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"enable_vertex_snap":{"doc_sha":"7401ad5dcd82024c","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"entities":{"doc_sha":"c2b185a3b718cc6b","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"find_path":{"doc_sha":"ebd174d40557cc19","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"find_path(start, end) -> list"},"fog_color":{"doc_sha":"3eb0cdbd75c5a94c","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"fog_far":{"doc_sha":"90406f8cae40bb58","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"fog_near":{"doc_sha":"e49f6dc79b22a794","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"follow":{"doc_sha":"975d2b945069be26","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"follow(entity, distance=10, height=5, smoothing=1.0)"},"fov":{"doc_sha":"7f5d69a1c423f69d","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"get_billboard":{"doc_sha":"38b0410480ab7d4d","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"get_billboard(index) -> Billboard"},"get_layer":{"doc_sha":"92223f274b5312c3","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"get_layer(name) -> dict or None"},"global_bounds":{"doc_sha":"aa4de9a20df55c2b","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"global_position":{"doc_sha":"b8dabbecc9fccd2a","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"grid_size":{"doc_sha":"7b4dbfa47f259f02","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"h":{"doc_sha":"d7308cd01fffb4be","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"hovered":{"doc_sha":"af0d353ce08f7be0","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"is_in_fov":{"doc_sha":"81d50fbbe2df4c8e","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"is_in_fov(x, z) -> bool"},"layer_count":{"doc_sha":"8bfbb28860b18d51","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"layer_count() -> int"},"move":{"doc_sha":"5c74f429970a2d2b","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"move(dx, dy) or (delta) -> None"},"on_click":{"doc_sha":"b3b8de4e294d71b6","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"on_enter":{"doc_sha":"48c996802e262396","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"on_exit":{"doc_sha":"c14ef0d6d6d40b0e","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"on_move":{"doc_sha":"9c3e26818b5b25b0","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"opacity":{"doc_sha":"c1a7ef41e074519e","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"orbit_camera":{"doc_sha":"29e02de46a996495","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"orbit_camera(angle=0, distance=10, height=5)"},"parent":{"doc_sha":"171b0de4e295c55b","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"place_blocking":{"doc_sha":"9252fd1d594a3867","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"place_blocking(grid_pos, footprint, walkable=False, transparent=False)"},"pos":{"doc_sha":"c1693d7dd0fe875d","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"project_all_voxels_to_nav":{"doc_sha":"ac9e0ca27b557d65","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"project_all_voxels_to_nav(headroom=2)"},"project_voxel_to_nav":{"doc_sha":"1fdf58bc686f7511","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"project_voxel_to_nav(voxel_grid, headroom=2)"},"realign":{"doc_sha":"8dfa7333a49aa697","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"realign() -> None"},"remove_billboard":{"doc_sha":"bf68668896f86bb0","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"remove_billboard(billboard)"},"remove_layer":{"doc_sha":"f5fafdfe3d153aab","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"remove_layer(name) -> bool"},"remove_voxel_layer":{"doc_sha":"b88a956cacc361cb","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"remove_voxel_layer(voxel_grid) -> bool"},"render_resolution":{"doc_sha":"4b141c5648005f04","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"resize":{"doc_sha":"005a04f1d129ebe5","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"resize(width, height) or (size) -> None"},"screen_to_world":{"doc_sha":"ba31510737afd516","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"screen_to_world(x, y, y_plane=0.0) -> tuple or None"},"set_grid_size":{"doc_sha":"ee7d1626547614c8","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"set_grid_size(width, depth)"},"set_slope_cost":{"doc_sha":"dac6d3cd319f1016","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"set_slope_cost(max_slope=0.5, cost_multiplier=1.0)"},"visible":{"doc_sha":"49221a4ae830f3b8","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"voxel_layer_count":{"doc_sha":"c1ca5c90881bbaae","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"voxel_layer_count() -> int"},"w":{"doc_sha":"4c5bcc34301022a0","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"x":{"doc_sha":"cd81d9dc457285aa","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"y":{"doc_sha":"406570900a1cf520","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"z_index":{"doc_sha":"328d87ae44d9fb71","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""}}},"VoxelGrid":{"doc_sha":"6706e3f5276ce104","kind":"class","lifecycle":{"since":"0.2.8"},"members":{"add_material":{"doc_sha":"27dec1c992cf7b99","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"add_material(name, color=Color(255,255,255), sprite_index=-1, transparent=False, path_cost=1.0) -> int"},"cell_size":{"doc_sha":"81b167d608b2cec5","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"clear":{"doc_sha":"8ebe20948a33a84d","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"clear() -> None"},"copy_region":{"doc_sha":"e56c5d28d7fd4e86","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"copy_region(min_coord, max_coord) -> VoxelRegion"},"count_material":{"doc_sha":"c9ef38e1f2f29ab8","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"count_material(material) -> int"},"count_non_air":{"doc_sha":"275d512b30f5134a","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"count_non_air() -> int"},"depth":{"doc_sha":"21671acfc312aeeb","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"fill":{"doc_sha":"fff2fcdddba5ee8c","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"fill(material) -> None"},"fill_box":{"doc_sha":"7c3d982a275f095d","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"fill_box(min_coord, max_coord, material) -> None"},"fill_box_hollow":{"doc_sha":"432e8b518feee0a5","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"fill_box_hollow(min_coord, max_coord, material, thickness=1) -> None"},"fill_cylinder":{"doc_sha":"0b2b0b0826381df0","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"fill_cylinder(base_pos, radius, height, material) -> None"},"fill_noise":{"doc_sha":"567355400611d920","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"fill_noise(min_coord, max_coord, material, threshold=0.5, scale=0.1, seed=0) -> None"},"fill_sphere":{"doc_sha":"e6a85d9c72e2ea44","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"fill_sphere(center, radius, material) -> None"},"from_bytes":{"doc_sha":"faefede0aa8ae636","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"from_bytes(data) -> bool"},"get":{"doc_sha":"52cea4645f0ea3d0","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"get(x, y, z) -> int"},"get_material":{"doc_sha":"2a61f6d54086df97","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"get_material(id) -> dict"},"greedy_meshing":{"doc_sha":"17fffa950c043917","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"height":{"doc_sha":"f271e989afe69da3","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"load":{"doc_sha":"5dcaf71057f836a2","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"load(path) -> bool"},"material_count":{"doc_sha":"0ef72bd68ef0c2d0","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"offset":{"doc_sha":"75230579ca06d8f5","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"paste_region":{"doc_sha":"1f44bacefd614cca","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"paste_region(region, position, skip_air=True) -> None"},"project_column":{"doc_sha":"9bacd59e3a7c5758","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"project_column(x, z, headroom=2) -> dict"},"rebuild_mesh":{"doc_sha":"fa60051037c3328c","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"rebuild_mesh() -> None"},"rotation":{"doc_sha":"5b603e8c652baf50","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"save":{"doc_sha":"bb7090694292f8e3","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"save(path) -> bool"},"set":{"doc_sha":"e9223501162aa9c6","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"set(x, y, z, material) -> None"},"size":{"doc_sha":"2d283d0584913b16","kind":"property","lifecycle":{"since":"0.2.8"},"signature":"Dimensions (width, height, depth) of the grid. Read-only."},"to_bytes":{"doc_sha":"f9a751e75bfc0478","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"to_bytes() -> bytes"},"vertex_count":{"doc_sha":"4df863704c32ecb7","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"visible":{"doc_sha":"7ff1afdbd4bf86a3","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"width":{"doc_sha":"0977aff160584918","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""}}},"VoxelRegion":{"doc_sha":"0cda0630ba7cafef","kind":"class","lifecycle":{"since":"0.2.8"},"members":{"depth":{"doc_sha":"05b3223f7df8ab65","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"height":{"doc_sha":"04834a15ca591472","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"size":{"doc_sha":"75c252385afbc498","kind":"property","lifecycle":{"since":"0.2.8"},"signature":"Dimensions (width, height, depth) of the region. Read-only."},"width":{"doc_sha":"d10d49e58037417d","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""}}},"WangSet":{"doc_sha":"320e161ef3d08a5d","kind":"class","lifecycle":{"since":"0.2.8"},"members":{"apply":{"doc_sha":"2aa8c0ba2d6549d2","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"apply(discrete_map: DiscreteMap, tile_layer: TileLayer) -> None"},"color_count":{"doc_sha":"13b244a538aec70f","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"colors":{"doc_sha":"6d84b3c2dd048b1a","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"name":{"doc_sha":"8b4560b705e5724f","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"resolve":{"doc_sha":"fe2fa09430c4bda6","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"resolve(discrete_map: DiscreteMap) -> list[int]"},"terrain_enum":{"doc_sha":"b8c46ec1a2b03eb6","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"terrain_enum() -> IntEnum"},"type":{"doc_sha":"ea3706e2c65ba7d3","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""}}},"Window":{"doc_sha":"6021e9b58544c378","kind":"class","lifecycle":{"since":"0.2.8"},"members":{"center":{"doc_sha":"91fe1b0d6ca2e4f1","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"center() -> None"},"framerate_limit":{"doc_sha":"de182ed271fbb4d3","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"fullscreen":{"doc_sha":"a517f5331026f2c4","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"game_resolution":{"doc_sha":"3a0dba59fecf5a95","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"get":{"doc_sha":"544bdc10ab93d067","kind":"classmethod","lifecycle":{"since":"0.2.8"},"signature":"get() -> Window"},"resolution":{"doc_sha":"548abee10c4fbcaf","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"scaling_mode":{"doc_sha":"57e03f9ffc9291ae","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"screenshot":{"doc_sha":"5102ab9c4c2c8358","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"screenshot(filename: str = None) -> bytes | None"},"title":{"doc_sha":"0d98279e5ec1b8b1","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"visible":{"doc_sha":"bad7f81ec2e48a31","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"vsync":{"doc_sha":"0335eb1fba386e37","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""}}}},"schema":1,"seeded":false,"version":"0.2.8-dev"} diff --git a/src/CommandLineParser.cpp b/src/CommandLineParser.cpp index 48be54d..9626a16 100644 --- a/src/CommandLineParser.cpp +++ b/src/CommandLineParser.cpp @@ -184,6 +184,8 @@ void CommandLineParser::print_help() { << "McRogueFace specific options:\n" << " --exec file : execute script before main program (can be used multiple times)\n" << " --headless : run without creating a window (implies --audio-off)\n" + << " --run-forever : keep running after --exec scripts finish, instead of\n" + << " exiting (for a long-lived headless process)\n" << " --audio-off : disable audio\n" << " --audio-on : enable audio (even in headless mode)\n" << " --screenshot [path] : take a screenshot in headless mode\n" diff --git a/src/McRFPy_API.cpp b/src/McRFPy_API.cpp index 7bdc989..00e0a0c 100644 --- a/src/McRFPy_API.cpp +++ b/src/McRFPy_API.cpp @@ -330,11 +330,22 @@ static PyMethodDef mcrfpyMethods[] = { {"step", McRFPy_API::_step, METH_VARARGS, MCRF_METHOD(mcrfpy, step, MCRF_SIG("(dt: float = None)", "float"), - MCRF_DESC("Advance simulation time (headless mode only)."), + MCRF_DESC("Run one full simulation frame (headless mode only). Advances the scene " + "update, timers, Python Scene.update() callbacks, animations, and scene " + "transitions (including their completion), records frame-time metrics, and " + "increments the frame counter -- everything a windowed frame does except " + "render and input.") MCRF_ARGS_START MCRF_ARG("dt", "Time to advance in seconds. If None, advances to the next scheduled event (timer/animation).") MCRF_RETURNS("float: Actual time advanced in seconds. Returns 0.0 in windowed mode.") - MCRF_NOTE("In windowed mode, this is a no-op and returns 0.0. Use this for deterministic simulation control in headless/testing scenarios.") + MCRF_NOTE("Rendering is deliberately excluded: it is orthogonal to the clock and costs " + "zero simulation time, so a screenshot draws arbitrary state without time " + "passing. In windowed mode this is a no-op returning 0.0. Timers run on " + "simulation time -- explicit, deterministic time is the point of driving a " + "headless test with step().") + MCRF_NOTE("A headless --exec script must end by calling sys.exit(), because step() is " + "the only headless clock: without it the engine cannot advance itself. Pass " + "--run-forever for a long-lived headless process that drives itself.") )}, {"exit", McRFPy_API::_exit, METH_NOARGS, MCRF_METHOD(mcrfpy, exit, @@ -1076,6 +1087,20 @@ PyStatus McRFPy_API::init_python_with_config(const McRogueFaceConfig& config) } PyStatus status; + + // #378: pre-initialize in UTF-8 mode. Without this, the filesystem encoding falls + // back to ASCII -- so open() with no explicit encoding= cannot read a UTF-8 file, + // even though sys.getdefaultencoding() and the locale both say UTF-8. This is the + // init path main.cpp uses; init_python() above has always done this correctly. + PyPreConfig preconfig; + PyPreConfig_InitIsolatedConfig(&preconfig); + preconfig.utf8_mode = 1; + + status = Py_PreInitialize(&preconfig); + if (PyStatus_Exception(status)) { + return status; + } + PyConfig pyconfig; PyConfig_InitIsolatedConfig(&pyconfig); diff --git a/tests/regression/issue_378_utf8_source_test.py b/tests/regression/issue_378_utf8_source_test.py new file mode 100644 index 0000000..0e3ea61 --- /dev/null +++ b/tests/regression/issue_378_utf8_source_test.py @@ -0,0 +1,85 @@ +#!/usr/bin/env python3 +""" +Issue #378: the embedded interpreter's filesystem encoding must be UTF-8. + +init_python_with_config() -- the path main.cpp actually uses -- did no +pre-initialization at all, so PyPreConfig.utf8_mode was never enabled and the +filesystem encoding fell back to ASCII. sys.getdefaultencoding() said utf-8 and the +locale said UTF-8; only the filesystem encoding disagreed. (init_python(), the other +init path, had always set utf8_mode = 1 correctly.) + +open() with no explicit encoding= defaults to the filesystem encoding, so it could not +read a UTF-8 file -- a degree sign was enough: + + UnicodeDecodeError: 'ascii' codec can't decode byte 0xc2 ... + +In any normal CPython 3, open() defaults to UTF-8. This broke any script reading a data +file, a save file, or its own source. + +NOT affected, despite the folklore that "--exec scripts must be ASCII-only": --exec on +a source file containing non-ASCII characters always worked. The C++ side reads the +file and hands the bytes to Python, which parses them as UTF-8 per PEP 3120. The +folklore was pointing at open(), via harnesses that read scripts themselves. + +This file contains non-ASCII characters and reads itself back with a bare open(), so it +exercises both halves. +""" + +import sys + +failures = [] + + +def check(cond, msg): + if not cond: + failures.append(msg) + print(f"FAIL: {msg}") + else: + print(f" ok: {msg}") + + +# Non-ASCII in a comment -- rotate the camera 90 degrees clockwise +# The characters that broke the docs snippets: degree, bullet +DEGREE = "\N{DEGREE SIGN}" +BULLET = "\N{BULLET}" + + +def test_filesystem_encoding(): + enc = sys.getfilesystemencoding() + check(enc.lower().replace("-", "") == "utf8", + f"sys.getfilesystemencoding() is utf-8 (got {enc!r})") + check(sys.getdefaultencoding().lower().replace("-", "") == "utf8", + "sys.getdefaultencoding() is utf-8") + + +def test_source_literals_survived_the_parse(): + """If the loader had used ASCII, this file would not have parsed at all.""" + check(DEGREE == "°", "a degree sign round-trips through the source") + check(BULLET == "•", "a bullet round-trips through the source") + + +def test_open_defaults_to_utf8(): + """open() with no encoding= must read UTF-8, like any other Python 3.""" + path = __file__ + with open(path) as f: + text = f.read() + check("DEGREE SIGN" in text, + "open() with no encoding= read this file back without a UnicodeDecodeError") + + +def main(): + test_filesystem_encoding() + test_source_literals_survived_the_parse() + test_open_defaults_to_utf8() + + if failures: + print(f"\nFAILED ({len(failures)} checks)") + for f in failures: + print(f" - {f}") + sys.exit(1) + print("\nPASS") + sys.exit(0) + + +if __name__ == "__main__": + main() From a46667df6f91bc74707ecede93a2856a9ffb9d1a Mon Sep 17 00:00:00 2001 From: John McCardle Date: Tue, 14 Jul 2026 18:29:37 -0400 Subject: [PATCH 2/5] test(snippets): the docs site's 130 code samples are now part of the test suite The samples published on mcrogueface.github.io were executed by nothing at all. Each carried a machine-readable header -- objects=[...] verified=0.2.8@b6720f6 status=ok -- and every one of those fields was hand-typed. 130 snippets asserted "status=ok" while no run had ever confirmed it. This is the same failure mode as the 82 rotted tests in 112f357, at a larger scale and with no gate whatsoever. The samples now live here, in the engine repo, so that breaking the API breaks the build. The site pulls them from tests/snippets/ rather than keeping a copy to rot in parallel (build_library.py, site-side). They are display scripts: each builds a scene and stops. They deliberately do NOT call sys.exit(), because they must stay copy-pasteable -- a reader who pastes one into their game should get a running scene, not an interpreter that quits. Since #350 that also makes them un-runnable on their own, so _harness.py is chained as a second --exec to supply the ending the snippet must not have. Chaining rather than exec()-ing the source is deliberate: the snippet travels the same path a real user's script travels, and nothing has to read the file -- which is how the first draft of this harness tripped over #378 and mistook three healthy snippets for broken ones. A snippet passes only if it runs clean AND leaves a scene with something in it. Not raising is not enough: a sample whose body silently no-ops still renders as code on the docs site, and would still be wrong. tools/stamp_snippets.py makes the header a measurement instead of a claim: status comes from an actual run, verified from the engine that ran it, and objects is derived from the source intersected with what the engine exports -- so a snippet that starts using a new type gets tagged for it without anyone remembering to. It immediately found tags the humans had missed (062 uses Transition, Key and InputState; the hand-written header listed none of them). --check is the CI gate. The gate earned its keep on the first run, catching #379 (fixed here): configuring default_transition before the first scene activation made the engine transition FROM the internal "uitest" bootstrap scene, which is not a PyScene and has no Python wrapper -- so mcrfpy.current_scene reported None for the transition's full duration, immediately after the user had assigned it. A transition with nothing meaningful to transition from now changes scene immediately. Scene-to-scene transitions still report the outgoing scene until they complete. Suite: 468/468 (336 + 130 snippets + 2 regressions). closes #379 --- api/manifest.json | 2 +- src/GameEngine.cpp | 16 +- .../issue_379_first_scene_transition_test.py | 104 +++++++++++ tests/run_tests.py | 19 +- tests/snippets/001_hello_frame.py | 17 ++ tests/snippets/002_hello_caption.py | 16 ++ tests/snippets/003_hello_sprite.py | 19 ++ tests/snippets/004_hello_grid.py | 25 +++ tests/snippets/005_hello_entity.py | 29 +++ tests/snippets/006_frame_colors.py | 33 ++++ tests/snippets/007_frame_opacity.py | 34 ++++ tests/snippets/008_frame_outline.py | 22 +++ tests/snippets/009_frame_children.py | 41 +++++ tests/snippets/010_frame_clip_children.py | 42 +++++ tests/snippets/011_frame_z_index.py | 48 +++++ tests/snippets/012_frame_visible.py | 32 ++++ tests/snippets/013_frame_name.py | 41 +++++ tests/snippets/014_frame_rotation.py | 29 +++ tests/snippets/015_frame_origin.py | 45 +++++ tests/snippets/016_frame_alignment.py | 43 +++++ tests/snippets/017_caption_styling.py | 26 +++ tests/snippets/018_caption_in_frame.py | 34 ++++ tests/snippets/019_caption_dimensions.py | 36 ++++ tests/snippets/020_sprite_atlas.py | 28 +++ tests/snippets/021_sprite_scale.py | 24 +++ tests/snippets/022_sprite_tint.py | 32 ++++ tests/snippets/023_grid_tiles.py | 39 ++++ tests/snippets/024_grid_walkable.py | 37 ++++ tests/snippets/025_grid_cell_color.py | 39 ++++ tests/snippets/026_grid_camera_center.py | 60 ++++++ tests/snippets/027_grid_zoom.py | 41 +++++ tests/snippets/028_entity_movement.py | 49 +++++ tests/snippets/029_entity_multiple.py | 44 +++++ tests/snippets/030_entity_draw_pos.py | 48 +++++ tests/snippets/031_animation_basic.py | 38 ++++ tests/snippets/032_animation_easing_linear.py | 42 +++++ .../snippets/033_animation_easing_ease_in.py | 34 ++++ .../snippets/034_animation_easing_ease_out.py | 34 ++++ .../035_animation_easing_ease_in_out.py | 34 ++++ tests/snippets/036_animation_easing_bounce.py | 37 ++++ .../snippets/037_animation_easing_elastic.py | 36 ++++ tests/snippets/038_animation_easing_back.py | 34 ++++ .../039_animation_easing_comparison.py | 43 +++++ tests/snippets/040_animation_color.py | 34 ++++ tests/snippets/041_animation_opacity.py | 38 ++++ tests/snippets/042_animation_size.py | 37 ++++ tests/snippets/043_animation_sprite_index.py | 34 ++++ tests/snippets/044_animation_callback.py | 37 ++++ tests/snippets/045_animation_chain.py | 46 +++++ tests/snippets/046_timer_basic.py | 32 ++++ tests/snippets/047_timer_once.py | 29 +++ tests/snippets/048_timer_control.py | 51 ++++++ tests/snippets/049_input_keyboard.py | 38 ++++ tests/snippets/050_input_mouse_click.py | 41 +++++ tests/snippets/051_input_mouse_hover.py | 45 +++++ tests/snippets/052_input_grid_cell_click.py | 47 +++++ tests/snippets/053_input_grid_cell_hover.py | 40 ++++ tests/snippets/054_pathfinding_astar.py | 56 ++++++ tests/snippets/055_pathfinding_entity.py | 54 ++++++ tests/snippets/056_pathfinding_dijkstra.py | 52 ++++++ tests/snippets/057_fov_basic.py | 58 ++++++ tests/snippets/058_fov_algorithms.py | 60 ++++++ tests/snippets/059_fov_perspective.py | 53 ++++++ tests/snippets/060_bsp_basic.py | 56 ++++++ tests/snippets/061_scene_switching.py | 34 ++++ tests/snippets/062_scene_transition_fade.py | 36 ++++ tests/snippets/063_scene_transition_slide.py | 35 ++++ tests/snippets/064_vector_basics.py | 50 +++++ tests/snippets/065_color_components.py | 40 ++++ tests/snippets/066_find_elements.py | 36 ++++ tests/snippets/067_collection_iteration.py | 40 ++++ tests/snippets/068_entity_collection.py | 41 +++++ tests/snippets/069_window_properties.py | 33 ++++ tests/snippets/070_keyboard_state.py | 46 +++++ tests/snippets/071_mouse_state.py | 39 ++++ tests/snippets/072_ui_button.py | 47 +++++ tests/snippets/073_ui_health_bar.py | 64 +++++++ tests/snippets/074_ui_tooltip.py | 52 ++++++ tests/snippets/075_ui_message_log.py | 62 +++++++ tests/snippets/076_effect_screen_shake.py | 55 ++++++ tests/snippets/077_effect_damage_flash.py | 55 ++++++ tests/snippets/078_effect_floating_text.py | 50 +++++ tests/snippets/079_effect_pulse.py | 39 ++++ tests/snippets/080_game_turn_based.py | 67 +++++++ tests/snippets/081_game_inventory.py | 57 ++++++ tests/snippets/082_game_minimap.py | 66 +++++++ tests/snippets/083_grid_layers.py | 43 +++++ tests/snippets/084_bresenham_line.py | 46 +++++ tests/snippets/085_entity_visibility.py | 50 +++++ tests/snippets/086_procgen_cellular.py | 59 ++++++ tests/snippets/087_grid_point_state.py | 54 ++++++ tests/snippets/088_animation_multiple.py | 49 +++++ tests/snippets/089_camera_follow.py | 60 ++++++ tests/snippets/090_scene_lifecycle.py | 45 +++++ tests/snippets/091_metrics_display.py | 44 +++++ tests/snippets/092_global_pos.py | 48 +++++ tests/snippets/093_parent_reference.py | 40 ++++ tests/snippets/094_entity_die.py | 48 +++++ tests/snippets/095_entity_index.py | 48 +++++ tests/snippets/096_timer_remaining.py | 53 ++++++ tests/snippets/097_hovered_state.py | 39 ++++ tests/snippets/098_grid_fill_color.py | 39 ++++ tests/snippets/099_scene_pos.py | 40 ++++ tests/snippets/100_complete_game.py | 76 ++++++++ tests/snippets/101_grid_camera_rotation.py | 46 +++++ tests/snippets/102_bsp_traversal.py | 65 +++++++ tests/snippets/103_animation_pos_vector.py | 43 +++++ tests/snippets/104_texture_properties.py | 49 +++++ tests/snippets/105_scenes_dict.py | 35 ++++ tests/snippets/106_timers_list.py | 45 +++++ tests/snippets/107_animations_list.py | 56 ++++++ tests/snippets/108_cache_subtree.py | 54 ++++++ tests/snippets/109_realign.py | 48 +++++ tests/snippets/110_version_info.py | 44 +++++ tests/snippets/111_alignment_margins.py | 43 +++++ tests/snippets/112_animation_delta.py | 44 +++++ tests/snippets/113_easing_cubic.py | 50 +++++ tests/snippets/114_easing_sine.py | 50 +++++ tests/snippets/115_easing_expo.py | 50 +++++ tests/snippets/116_easing_circ.py | 40 ++++ tests/snippets/117_easing_quart.py | 40 ++++ tests/snippets/118_ui_modal.py | 65 +++++++ tests/snippets/119_game_collision.py | 68 +++++++ tests/snippets/120_animation_complete.py | 45 +++++ tests/snippets/121_entity_at.py | 48 +++++ tests/snippets/122_default_fov.py | 53 ++++++ tests/snippets/123_animation_stop.py | 38 ++++ tests/snippets/124_sprite_rotation.py | 32 ++++ tests/snippets/125_vector_normalized.py | 46 +++++ tests/snippets/126_timer_restart.py | 48 +++++ tests/snippets/127_grid_size.py | 46 +++++ .../snippets/128_color_animation_channels.py | 75 ++++++++ tests/snippets/129_scene_update.py | 51 ++++++ tests/snippets/130_exit_game.py | 50 +++++ tests/snippets/_harness.py | 60 ++++++ tools/stamp_snippets.py | 173 ++++++++++++++++++ 136 files changed, 6084 insertions(+), 5 deletions(-) create mode 100644 tests/regression/issue_379_first_scene_transition_test.py create mode 100644 tests/snippets/001_hello_frame.py create mode 100644 tests/snippets/002_hello_caption.py create mode 100644 tests/snippets/003_hello_sprite.py create mode 100644 tests/snippets/004_hello_grid.py create mode 100644 tests/snippets/005_hello_entity.py create mode 100644 tests/snippets/006_frame_colors.py create mode 100644 tests/snippets/007_frame_opacity.py create mode 100644 tests/snippets/008_frame_outline.py create mode 100644 tests/snippets/009_frame_children.py create mode 100644 tests/snippets/010_frame_clip_children.py create mode 100644 tests/snippets/011_frame_z_index.py create mode 100644 tests/snippets/012_frame_visible.py create mode 100644 tests/snippets/013_frame_name.py create mode 100644 tests/snippets/014_frame_rotation.py create mode 100644 tests/snippets/015_frame_origin.py create mode 100644 tests/snippets/016_frame_alignment.py create mode 100644 tests/snippets/017_caption_styling.py create mode 100644 tests/snippets/018_caption_in_frame.py create mode 100644 tests/snippets/019_caption_dimensions.py create mode 100644 tests/snippets/020_sprite_atlas.py create mode 100644 tests/snippets/021_sprite_scale.py create mode 100644 tests/snippets/022_sprite_tint.py create mode 100644 tests/snippets/023_grid_tiles.py create mode 100644 tests/snippets/024_grid_walkable.py create mode 100644 tests/snippets/025_grid_cell_color.py create mode 100644 tests/snippets/026_grid_camera_center.py create mode 100644 tests/snippets/027_grid_zoom.py create mode 100644 tests/snippets/028_entity_movement.py create mode 100644 tests/snippets/029_entity_multiple.py create mode 100644 tests/snippets/030_entity_draw_pos.py create mode 100644 tests/snippets/031_animation_basic.py create mode 100644 tests/snippets/032_animation_easing_linear.py create mode 100644 tests/snippets/033_animation_easing_ease_in.py create mode 100644 tests/snippets/034_animation_easing_ease_out.py create mode 100644 tests/snippets/035_animation_easing_ease_in_out.py create mode 100644 tests/snippets/036_animation_easing_bounce.py create mode 100644 tests/snippets/037_animation_easing_elastic.py create mode 100644 tests/snippets/038_animation_easing_back.py create mode 100644 tests/snippets/039_animation_easing_comparison.py create mode 100644 tests/snippets/040_animation_color.py create mode 100644 tests/snippets/041_animation_opacity.py create mode 100644 tests/snippets/042_animation_size.py create mode 100644 tests/snippets/043_animation_sprite_index.py create mode 100644 tests/snippets/044_animation_callback.py create mode 100644 tests/snippets/045_animation_chain.py create mode 100644 tests/snippets/046_timer_basic.py create mode 100644 tests/snippets/047_timer_once.py create mode 100644 tests/snippets/048_timer_control.py create mode 100644 tests/snippets/049_input_keyboard.py create mode 100644 tests/snippets/050_input_mouse_click.py create mode 100644 tests/snippets/051_input_mouse_hover.py create mode 100644 tests/snippets/052_input_grid_cell_click.py create mode 100644 tests/snippets/053_input_grid_cell_hover.py create mode 100644 tests/snippets/054_pathfinding_astar.py create mode 100644 tests/snippets/055_pathfinding_entity.py create mode 100644 tests/snippets/056_pathfinding_dijkstra.py create mode 100644 tests/snippets/057_fov_basic.py create mode 100644 tests/snippets/058_fov_algorithms.py create mode 100644 tests/snippets/059_fov_perspective.py create mode 100644 tests/snippets/060_bsp_basic.py create mode 100644 tests/snippets/061_scene_switching.py create mode 100644 tests/snippets/062_scene_transition_fade.py create mode 100644 tests/snippets/063_scene_transition_slide.py create mode 100644 tests/snippets/064_vector_basics.py create mode 100644 tests/snippets/065_color_components.py create mode 100644 tests/snippets/066_find_elements.py create mode 100644 tests/snippets/067_collection_iteration.py create mode 100644 tests/snippets/068_entity_collection.py create mode 100644 tests/snippets/069_window_properties.py create mode 100644 tests/snippets/070_keyboard_state.py create mode 100644 tests/snippets/071_mouse_state.py create mode 100644 tests/snippets/072_ui_button.py create mode 100644 tests/snippets/073_ui_health_bar.py create mode 100644 tests/snippets/074_ui_tooltip.py create mode 100644 tests/snippets/075_ui_message_log.py create mode 100644 tests/snippets/076_effect_screen_shake.py create mode 100644 tests/snippets/077_effect_damage_flash.py create mode 100644 tests/snippets/078_effect_floating_text.py create mode 100644 tests/snippets/079_effect_pulse.py create mode 100644 tests/snippets/080_game_turn_based.py create mode 100644 tests/snippets/081_game_inventory.py create mode 100644 tests/snippets/082_game_minimap.py create mode 100644 tests/snippets/083_grid_layers.py create mode 100644 tests/snippets/084_bresenham_line.py create mode 100644 tests/snippets/085_entity_visibility.py create mode 100644 tests/snippets/086_procgen_cellular.py create mode 100644 tests/snippets/087_grid_point_state.py create mode 100644 tests/snippets/088_animation_multiple.py create mode 100644 tests/snippets/089_camera_follow.py create mode 100644 tests/snippets/090_scene_lifecycle.py create mode 100644 tests/snippets/091_metrics_display.py create mode 100644 tests/snippets/092_global_pos.py create mode 100644 tests/snippets/093_parent_reference.py create mode 100644 tests/snippets/094_entity_die.py create mode 100644 tests/snippets/095_entity_index.py create mode 100644 tests/snippets/096_timer_remaining.py create mode 100644 tests/snippets/097_hovered_state.py create mode 100644 tests/snippets/098_grid_fill_color.py create mode 100644 tests/snippets/099_scene_pos.py create mode 100644 tests/snippets/100_complete_game.py create mode 100644 tests/snippets/101_grid_camera_rotation.py create mode 100644 tests/snippets/102_bsp_traversal.py create mode 100644 tests/snippets/103_animation_pos_vector.py create mode 100644 tests/snippets/104_texture_properties.py create mode 100644 tests/snippets/105_scenes_dict.py create mode 100644 tests/snippets/106_timers_list.py create mode 100644 tests/snippets/107_animations_list.py create mode 100644 tests/snippets/108_cache_subtree.py create mode 100644 tests/snippets/109_realign.py create mode 100644 tests/snippets/110_version_info.py create mode 100644 tests/snippets/111_alignment_margins.py create mode 100644 tests/snippets/112_animation_delta.py create mode 100644 tests/snippets/113_easing_cubic.py create mode 100644 tests/snippets/114_easing_sine.py create mode 100644 tests/snippets/115_easing_expo.py create mode 100644 tests/snippets/116_easing_circ.py create mode 100644 tests/snippets/117_easing_quart.py create mode 100644 tests/snippets/118_ui_modal.py create mode 100644 tests/snippets/119_game_collision.py create mode 100644 tests/snippets/120_animation_complete.py create mode 100644 tests/snippets/121_entity_at.py create mode 100644 tests/snippets/122_default_fov.py create mode 100644 tests/snippets/123_animation_stop.py create mode 100644 tests/snippets/124_sprite_rotation.py create mode 100644 tests/snippets/125_vector_normalized.py create mode 100644 tests/snippets/126_timer_restart.py create mode 100644 tests/snippets/127_grid_size.py create mode 100644 tests/snippets/128_color_animation_channels.py create mode 100644 tests/snippets/129_scene_update.py create mode 100644 tests/snippets/130_exit_game.py create mode 100644 tests/snippets/_harness.py create mode 100644 tools/stamp_snippets.py diff --git a/api/manifest.json b/api/manifest.json index 79666b5..f6ba81a 100644 --- a/api/manifest.json +++ b/api/manifest.json @@ -1 +1 @@ -{"commit":"a7ba486","dirty":true,"functions":{"bresenham":{"doc_sha":"ee2d0f7a54a664f5","kind":"function","lifecycle":{"since":"0.2.8"},"signature":"bresenham(start, end, include_start=True, include_end=True) -> list[tuple[int, int]]"},"end_benchmark":{"doc_sha":"5d783c5dabb7a4e9","kind":"function","lifecycle":{"since":"0.2.8"},"signature":"end_benchmark() -> str"},"exit":{"doc_sha":"7572d4d6b1a1c8a4","kind":"function","lifecycle":{"since":"0.2.8"},"signature":"exit() -> None"},"find":{"doc_sha":"364b95807edcf8b4","kind":"function","lifecycle":{"since":"0.2.8"},"signature":"find(name: str, scene: str = None) -> UIDrawable | None"},"find_all":{"doc_sha":"b4a278f16b172131","kind":"function","lifecycle":{"since":"0.2.8"},"signature":"find_all(pattern: str, scene: str = None) -> list"},"get_metrics":{"doc_sha":"a0bbee3858baa4a9","kind":"function","lifecycle":{"modified":{"commit":"464af6a","version":"0.2.8-dev"},"since":"0.2.8"},"signature":"get_metrics() -> dict"},"lock":{"doc_sha":"e42989cabc81d080","kind":"function","lifecycle":{"since":"0.2.8"},"signature":"lock() -> _LockContext"},"log_benchmark":{"doc_sha":"4564bf7321366a0a","kind":"function","lifecycle":{"since":"0.2.8"},"signature":"log_benchmark(message: str) -> None"},"set_dev_console":{"doc_sha":"940ed19ff4f5243f","kind":"function","lifecycle":{"since":"0.2.8"},"signature":"set_dev_console(enabled: bool) -> None"},"set_scale":{"doc_sha":"b565b441a326af48","kind":"function","lifecycle":{"since":"0.2.8"},"signature":"set_scale(multiplier: float) -> None"},"start_benchmark":{"doc_sha":"783957f3517518fe","kind":"function","lifecycle":{"since":"0.2.8"},"signature":"start_benchmark() -> None"},"step":{"doc_sha":"94e778b7e791028a","kind":"function","lifecycle":{"modified":{"commit":"a7ba486","version":"0.2.8-dev"},"since":"0.2.8"},"signature":"step(dt: float = None) -> float"}},"module_attributes":{"animations":{"doc_sha":"30d25725feac9eb6","kind":"attribute","lifecycle":{"since":"0.2.8-dev"},"readonly":true},"current_scene":{"doc_sha":"ab496ea99fec2fa8","kind":"attribute","lifecycle":{"since":"0.2.8-dev"},"readonly":false},"default_transition":{"doc_sha":"73fb568e9cb25540","kind":"attribute","lifecycle":{"since":"0.2.8-dev"},"readonly":false},"default_transition_duration":{"doc_sha":"ae57530eded49138","kind":"attribute","lifecycle":{"since":"0.2.8-dev"},"readonly":false},"save_dir":{"doc_sha":"96ffa5a27d178b14","kind":"attribute","lifecycle":{"since":"0.2.8-dev"},"readonly":true},"scenes":{"doc_sha":"8021c54383d3cb8e","kind":"attribute","lifecycle":{"since":"0.2.8-dev"},"readonly":true},"timers":{"doc_sha":"48e2aa7747d8f527","kind":"attribute","lifecycle":{"since":"0.2.8-dev"},"readonly":true}},"objects":{"AStarPath":{"doc_sha":"60dd12bb4a54f3dd","kind":"class","lifecycle":{"since":"0.2.8"},"members":{"destination":{"doc_sha":"62a81c7267ad9ed9","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"origin":{"doc_sha":"c7dd130b8d4ef800","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"peek":{"doc_sha":"14054f10b4d9dda0","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"peek() -> Vector"},"remaining":{"doc_sha":"c193adc4bbfa3564","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"walk":{"doc_sha":"3fb64b239109d301","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"walk() -> Vector"}}},"Alignment":{"doc_sha":"77d93d03c8994f84","kind":"class","lifecycle":{"since":"0.2.8"},"members":{"as_integer_ratio":{"doc_sha":"3eb115fab0d0e0ec","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"($self, /)"},"bit_count":{"doc_sha":"f276da700b5a2e37","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"($self, /)"},"bit_length":{"doc_sha":"15b29db62fd0fa6c","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"($self, /)"},"conjugate":{"doc_sha":"6517a002faad7c75","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"($self, /)"},"denominator":{"doc_sha":"5051eb1570768851","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"from_bytes":{"doc_sha":"64672607865643a7","kind":"classmethod","lifecycle":{"since":"0.2.8"},"signature":"($type, /, bytes, byteorder='big', *, signed=False)"},"imag":{"doc_sha":"ef78ea410df39b26","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"is_integer":{"doc_sha":"6176763fbac2fd22","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"($self, /)"},"numerator":{"doc_sha":"ed255748ce2eda7c","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"real":{"doc_sha":"d44497f1990af6fe","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"to_bytes":{"doc_sha":"b452319e48e64764","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"($self, /, length=1, byteorder='big', *, signed=False)"}}},"Arc":{"doc_sha":"8323f4b7101509e9","kind":"class","lifecycle":{"since":"0.2.8"},"members":{"align":{"doc_sha":"58657c3cfa010f31","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"animate":{"doc_sha":"400df4f5893261c5","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"animate(property: str, target: Any, duration: float, easing=None, delta=False, loop=False, callback=None, conflict_mode='replace') -> Animation"},"bounds":{"doc_sha":"d3186ac3b3ff8bed","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"center":{"doc_sha":"abf1643dda106bf8","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"color":{"doc_sha":"bdf932405b56c319","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"end_angle":{"doc_sha":"74f9704909a3a23d","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"global_bounds":{"doc_sha":"aa4de9a20df55c2b","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"global_position":{"doc_sha":"b8dabbecc9fccd2a","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"grid_pos":{"doc_sha":"9df77e9a996208ba","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"grid_size":{"doc_sha":"e32208817299dfe9","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"horiz_margin":{"doc_sha":"0ef227a4cae6e675","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"hovered":{"doc_sha":"af0d353ce08f7be0","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"margin":{"doc_sha":"ab774a7da8cfe9bf","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"move":{"doc_sha":"5c74f429970a2d2b","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"move(dx, dy) or (delta) -> None"},"name":{"doc_sha":"0cc0684f26d8a660","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"on_click":{"doc_sha":"f2ef657aaf8b6c68","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"on_enter":{"doc_sha":"48c996802e262396","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"on_exit":{"doc_sha":"c14ef0d6d6d40b0e","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"on_move":{"doc_sha":"9c3e26818b5b25b0","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"opacity":{"doc_sha":"c1a7ef41e074519e","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"origin":{"doc_sha":"270bdf366c74bb1d","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"parent":{"doc_sha":"171b0de4e295c55b","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"pos":{"doc_sha":"6612fa35c43918e7","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"radius":{"doc_sha":"f9c2310b775f1e5a","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"realign":{"doc_sha":"8dfa7333a49aa697","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"realign() -> None"},"resize":{"doc_sha":"005a04f1d129ebe5","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"resize(width, height) or (size) -> None"},"rotate_with_camera":{"doc_sha":"5c3af880218d3a08","kind":"property","lifecycle":{"modified":{"commit":"4bb1966","version":"0.2.8-dev"},"since":"0.2.8"},"signature":""},"rotation":{"doc_sha":"61ba3e66a0841a08","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"start_angle":{"doc_sha":"6b91f175ebe5ecb4","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"thickness":{"doc_sha":"f8eba89682554e6d","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"vert_margin":{"doc_sha":"e2042be923bc6fcb","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"visible":{"doc_sha":"49221a4ae830f3b8","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"z_index":{"doc_sha":"67dd517aa153c158","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""}}},"AutoRuleSet":{"doc_sha":"49ab716632a44f06","kind":"class","lifecycle":{"since":"0.2.8"},"members":{"apply":{"doc_sha":"701b6f038cfd6809","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"apply(discrete_map: DiscreteMap, tile_layer: TileLayer, seed: int = 0) -> None"},"grid_size":{"doc_sha":"570f4c81acb781d8","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"group_count":{"doc_sha":"892fc283939b3e0c","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"name":{"doc_sha":"4f5ca90860649b64","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"resolve":{"doc_sha":"f8fff4e6e6488e3f","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"resolve(discrete_map: DiscreteMap, seed: int = 0) -> list[int]"},"rule_count":{"doc_sha":"9b9d5a7c1475dcfa","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"terrain_enum":{"doc_sha":"eb365bf99bbcca37","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"terrain_enum() -> IntEnum"},"value_count":{"doc_sha":"3467d158dd5af073","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"values":{"doc_sha":"b3199e059fd1ca2a","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""}}},"BSP":{"doc_sha":"7e971e0542cf4006","kind":"class","lifecycle":{"since":"0.2.8"},"members":{"adjacency":{"doc_sha":"9373aa85408486ba","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"bounds":{"doc_sha":"9c41e7403f2a0b2f","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"clear":{"doc_sha":"2e4ffd3360ac858e","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"clear() -> BSP"},"find":{"doc_sha":"fccfac484a2b889c","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"find(pos: tuple[int, int] | list | Vector) -> BSPNode | None"},"get_leaf":{"doc_sha":"d8ba680ec2ad4fd1","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"get_leaf(index: int) -> BSPNode"},"leaves":{"doc_sha":"439ec78ec02e6df8","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"leaves() -> Iterator[BSPNode]"},"pos":{"doc_sha":"b8b27f4ad76e2870","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"root":{"doc_sha":"d1e3d9741dab383d","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"size":{"doc_sha":"621f11f9a37dd89d","kind":"property","lifecycle":{"since":"0.2.8"},"signature":"Dimensions (width, height). Read-only."},"split_once":{"doc_sha":"0d7c0c33156bc4e5","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"split_once(horizontal: bool, position: int) -> BSP"},"split_recursive":{"doc_sha":"fa516b79833e5094","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"split_recursive(depth: int, min_size: tuple[int, int], max_ratio: float = 1.5, seed: int | None = None) -> BSP"},"to_heightmap":{"doc_sha":"a0f03cab7a0f2d1b","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"to_heightmap(size: tuple[int, int] | None = None, select: str = 'leaves', shrink: int = 0, value: float = 1.0) -> HeightMap"},"traverse":{"doc_sha":"57da83e0f37686aa","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"traverse(order: Traversal = Traversal.LEVEL_ORDER) -> Iterator[BSPNode]"}}},"Behavior":{"doc_sha":"09206365e6c37ecc","kind":"class","lifecycle":{"since":"0.2.8"},"members":{"as_integer_ratio":{"doc_sha":"3eb115fab0d0e0ec","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"($self, /)"},"bit_count":{"doc_sha":"f276da700b5a2e37","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"($self, /)"},"bit_length":{"doc_sha":"15b29db62fd0fa6c","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"($self, /)"},"conjugate":{"doc_sha":"6517a002faad7c75","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"($self, /)"},"denominator":{"doc_sha":"5051eb1570768851","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"from_bytes":{"doc_sha":"64672607865643a7","kind":"classmethod","lifecycle":{"since":"0.2.8"},"signature":"($type, /, bytes, byteorder='big', *, signed=False)"},"imag":{"doc_sha":"ef78ea410df39b26","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"is_integer":{"doc_sha":"6176763fbac2fd22","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"($self, /)"},"numerator":{"doc_sha":"ed255748ce2eda7c","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"real":{"doc_sha":"d44497f1990af6fe","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"to_bytes":{"doc_sha":"b452319e48e64764","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"($self, /, length=1, byteorder='big', *, signed=False)"}}},"Billboard":{"doc_sha":"38614d1d19942bab","kind":"class","lifecycle":{"since":"0.2.8"},"members":{"facing":{"doc_sha":"f82e2d7d8087804a","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"opacity":{"doc_sha":"dc5ed39915ab896c","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"phi":{"doc_sha":"1450c89fb5b47929","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"pos":{"doc_sha":"0d5fec7fd4701ea6","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"scale":{"doc_sha":"c9d55c4579681854","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"sprite_index":{"doc_sha":"170c20c841b586fc","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"texture":{"doc_sha":"b6d846566b6ccf93","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"theta":{"doc_sha":"c214ecbcb099228b","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"visible":{"doc_sha":"adf8070be9a09eba","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""}}},"CallableBinding":{"doc_sha":"711bf0f149ccae7a","kind":"class","lifecycle":{"since":"0.2.8"},"members":{"callable":{"doc_sha":"96d3d31a6b7c56d9","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"is_valid":{"doc_sha":"bc69c90e13475ae6","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"value":{"doc_sha":"8f7d9eb4f520c017","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""}}},"Caption":{"doc_sha":"8cd46324baa46a0d","kind":"class","lifecycle":{"since":"0.2.8"},"members":{"align":{"doc_sha":"58657c3cfa010f31","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"animate":{"doc_sha":"400df4f5893261c5","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"animate(property: str, target: Any, duration: float, easing=None, delta=False, loop=False, callback=None, conflict_mode='replace') -> Animation"},"bounds":{"doc_sha":"d3186ac3b3ff8bed","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"fill_color":{"doc_sha":"af7ba90df95e4b79","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"font":{"doc_sha":"fbdafdb33921aa13","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"font_size":{"doc_sha":"7894149ec4efec6e","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"global_bounds":{"doc_sha":"aa4de9a20df55c2b","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"global_position":{"doc_sha":"b8dabbecc9fccd2a","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"grid_pos":{"doc_sha":"79a7d48503e657d1","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"grid_size":{"doc_sha":"25b029c86a618fee","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"h":{"doc_sha":"ca1a98d700ff1888","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"horiz_margin":{"doc_sha":"0ef227a4cae6e675","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"hovered":{"doc_sha":"af0d353ce08f7be0","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"margin":{"doc_sha":"ab774a7da8cfe9bf","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"move":{"doc_sha":"5c74f429970a2d2b","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"move(dx, dy) or (delta) -> None"},"name":{"doc_sha":"90a2ab0ec07c00eb","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"on_click":{"doc_sha":"b3b8de4e294d71b6","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"on_enter":{"doc_sha":"48c996802e262396","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"on_exit":{"doc_sha":"c14ef0d6d6d40b0e","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"on_move":{"doc_sha":"9c3e26818b5b25b0","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"opacity":{"doc_sha":"c1a7ef41e074519e","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"origin":{"doc_sha":"270bdf366c74bb1d","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"outline":{"doc_sha":"6580b31092048bcb","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"outline_color":{"doc_sha":"388919803d270fd3","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"parent":{"doc_sha":"171b0de4e295c55b","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"pos":{"doc_sha":"44e4967e15212959","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"realign":{"doc_sha":"8dfa7333a49aa697","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"realign() -> None"},"resize":{"doc_sha":"005a04f1d129ebe5","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"resize(width, height) or (size) -> None"},"rotate_with_camera":{"doc_sha":"5c3af880218d3a08","kind":"property","lifecycle":{"modified":{"commit":"4bb1966","version":"0.2.8-dev"},"since":"0.2.8"},"signature":""},"rotation":{"doc_sha":"61ba3e66a0841a08","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"shader":{"doc_sha":"418952064319ffb5","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"size":{"doc_sha":"85abb564299b703d","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"text":{"doc_sha":"41628142b287defe","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"uniforms":{"doc_sha":"b1f2699da618d002","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"vert_margin":{"doc_sha":"e2042be923bc6fcb","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"visible":{"doc_sha":"49221a4ae830f3b8","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"w":{"doc_sha":"8472810bca891509","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"x":{"doc_sha":"528487f8deaaf789","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"y":{"doc_sha":"114ecec940477107","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"z_index":{"doc_sha":"328d87ae44d9fb71","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""}}},"Circle":{"doc_sha":"077981a1fbdfb6e3","kind":"class","lifecycle":{"since":"0.2.8"},"members":{"align":{"doc_sha":"58657c3cfa010f31","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"animate":{"doc_sha":"400df4f5893261c5","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"animate(property: str, target: Any, duration: float, easing=None, delta=False, loop=False, callback=None, conflict_mode='replace') -> Animation"},"bounds":{"doc_sha":"d3186ac3b3ff8bed","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"center":{"doc_sha":"d1e80ccd6bf706b9","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"fill_color":{"doc_sha":"46be64336e296c50","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"global_bounds":{"doc_sha":"aa4de9a20df55c2b","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"global_position":{"doc_sha":"b8dabbecc9fccd2a","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"grid_pos":{"doc_sha":"20bebfa7432ed339","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"grid_size":{"doc_sha":"b43e33c2d49a3565","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"horiz_margin":{"doc_sha":"0ef227a4cae6e675","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"hovered":{"doc_sha":"af0d353ce08f7be0","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"margin":{"doc_sha":"ab774a7da8cfe9bf","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"move":{"doc_sha":"5c74f429970a2d2b","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"move(dx, dy) or (delta) -> None"},"name":{"doc_sha":"0cc0684f26d8a660","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"on_click":{"doc_sha":"3a340282000297e0","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"on_enter":{"doc_sha":"48c996802e262396","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"on_exit":{"doc_sha":"c14ef0d6d6d40b0e","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"on_move":{"doc_sha":"9c3e26818b5b25b0","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"opacity":{"doc_sha":"c1a7ef41e074519e","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"origin":{"doc_sha":"270bdf366c74bb1d","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"outline":{"doc_sha":"20e9fd42a7ba4bb8","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"outline_color":{"doc_sha":"3fbed883fee308fe","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"parent":{"doc_sha":"171b0de4e295c55b","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"pos":{"doc_sha":"5f9d88ff075d2cfc","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"radius":{"doc_sha":"00a41207192c55a8","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"realign":{"doc_sha":"8dfa7333a49aa697","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"realign() -> None"},"resize":{"doc_sha":"005a04f1d129ebe5","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"resize(width, height) or (size) -> None"},"rotate_with_camera":{"doc_sha":"5c3af880218d3a08","kind":"property","lifecycle":{"modified":{"commit":"4bb1966","version":"0.2.8-dev"},"since":"0.2.8"},"signature":""},"rotation":{"doc_sha":"61ba3e66a0841a08","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"vert_margin":{"doc_sha":"e2042be923bc6fcb","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"visible":{"doc_sha":"49221a4ae830f3b8","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"z_index":{"doc_sha":"0cb3df5278d2658b","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""}}},"Color":{"doc_sha":"f1f664405900332f","kind":"class","lifecycle":{"since":"0.2.8"},"members":{"a":{"doc_sha":"ef61630ad34f8e2a","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"b":{"doc_sha":"2e2e91e93833fdd0","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"from_hex":{"doc_sha":"24d1ff553133bb7e","kind":"classmethod","lifecycle":{"since":"0.2.8"},"signature":"from_hex(hex_string: str) -> Color"},"g":{"doc_sha":"716ced374bcc2dc4","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"lerp":{"doc_sha":"290e204f3538e406","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"lerp(other: Color, t: float) -> Color"},"r":{"doc_sha":"ef64cae6296f68cb","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"to_hex":{"doc_sha":"d0cf65b284158877","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"to_hex() -> str"}}},"ColorLayer":{"doc_sha":"f8a87b6184c9a9b6","kind":"class","lifecycle":{"since":"0.2.8"},"members":{"apply_gradient":{"doc_sha":"20390111f0652ae0","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"apply_gradient(source: HeightMap, range: tuple, color_low: Color, color_high: Color) -> ColorLayer"},"apply_perspective":{"doc_sha":"a909e1b1c0373bc0","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"apply_perspective(entity: Entity, visible: Color | None = None, discovered: Color | None = None, unknown: Color | None = None) -> None"},"apply_ranges":{"doc_sha":"37fc5a26fda9cc0c","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"apply_ranges(source: HeightMap, ranges: list) -> ColorLayer"},"apply_threshold":{"doc_sha":"59cc9b72322cc677","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"apply_threshold(source: HeightMap, range: tuple, color: Color) -> ColorLayer"},"at":{"doc_sha":"fef47313dcef9247","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"at(pos: tuple | Vector) or (x: int, y: int) -> Color"},"clear_perspective":{"doc_sha":"fec237fe4cbda340","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"clear_perspective() -> None"},"draw_fov":{"doc_sha":"ff965b12c888d092","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"draw_fov(source: tuple, radius: int | None = None, fov: FOV | None = None, visible: Color | None = None, discovered: Color | None = None, unknown: Color | None = None) -> None"},"edit":{"doc_sha":"b0edb9c8f4c1fe76","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"edit() -> context manager"},"fill":{"doc_sha":"f6ff376fd71ae90b","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"fill(color: Color) -> None"},"fill_rect":{"doc_sha":"bc8b20da214aa37d","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"fill_rect(pos: tuple, size: tuple, color: Color) -> None"},"grid":{"doc_sha":"b965c55d57de78fa","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"grid_size":{"doc_sha":"eac88eb0f7b16cbf","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"name":{"doc_sha":"dd282cc9273e2e36","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"set":{"doc_sha":"36801cc04606ee0b","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"set(pos, color: Color) -> None"},"update_perspective":{"doc_sha":"c9caac507fbc4921","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"update_perspective() -> None"},"visible":{"doc_sha":"ecea81ec16e31c19","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"z_index":{"doc_sha":"eb13c1b4de11ce63","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""}}},"DijkstraMap":{"doc_sha":"822ccf5b086d37ec","kind":"class","lifecycle":{"since":"0.2.8"},"members":{"descent_step":{"doc_sha":"fd7b649f1f3eb586","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"descent_step(pos: Vector | tuple) -> Vector | None"},"distance":{"doc_sha":"92b40108bb47bf88","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"distance(pos: Vector | tuple) -> float | None"},"invert":{"doc_sha":"3e5ae145dafc2c1d","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"invert() -> DijkstraMap"},"path_from":{"doc_sha":"3dcf638bff837f2d","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"path_from(pos: Vector | tuple) -> AStarPath"},"root":{"doc_sha":"95824bac9c46ad83","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"step_from":{"doc_sha":"ac9bc93f671d3936","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"step_from(pos: Vector | tuple) -> Vector | None"},"to_heightmap":{"doc_sha":"0ba542e6509b510b","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"to_heightmap(size=None, unreachable=-1.0) -> HeightMap"}}},"DiscreteMap":{"doc_sha":"4d6d00ffa3ac62a9","kind":"class","lifecycle":{"since":"0.2.8"},"members":{"add":{"doc_sha":"1e5a5b83f555fa87","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"add(other: DiscreteMap | int, *, pos=None, source_pos=None, size=None) -> DiscreteMap"},"bitwise_and":{"doc_sha":"1644af83b8699792","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"bitwise_and(other: DiscreteMap, *, pos=None, source_pos=None, size=None) -> DiscreteMap"},"bitwise_or":{"doc_sha":"c8262af2fdccdeeb","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"bitwise_or(other: DiscreteMap, *, pos=None, source_pos=None, size=None) -> DiscreteMap"},"bitwise_xor":{"doc_sha":"01809471595bf8e7","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"bitwise_xor(other: DiscreteMap, *, pos=None, source_pos=None, size=None) -> DiscreteMap"},"bool":{"doc_sha":"a999f0f09edf7bf3","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"bool(condition: int | set | callable) -> DiscreteMap"},"clear":{"doc_sha":"b04d176e9c7bf9d2","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"clear() -> DiscreteMap"},"copy_from":{"doc_sha":"efea5a0aa9a8d9c9","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"copy_from(other: DiscreteMap, *, pos=None, source_pos=None, size=None) -> DiscreteMap"},"count":{"doc_sha":"5e3303ddc3a9e3ae","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"count(value: int) -> int"},"count_range":{"doc_sha":"ac4e0298919fd16e","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"count_range(min_val: int, max_val: int) -> int"},"enum_type":{"doc_sha":"f27f1ad46e2d5afe","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"fill":{"doc_sha":"7f358bdcc5c2c9f0","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"fill(value: int, *, pos=None, size=None) -> DiscreteMap"},"from_bytes":{"doc_sha":"b1c09c737d43ffa9","kind":"classmethod","lifecycle":{"since":"0.2.8"},"signature":"from_bytes(data: bytes, size: tuple[int, int], *, enum: type = None) -> DiscreteMap"},"from_heightmap":{"doc_sha":"0e89bb4262b19ea3","kind":"classmethod","lifecycle":{"since":"0.2.8"},"signature":"from_heightmap(hmap: HeightMap, mapping: list[tuple[tuple[float,float], int]], *, enum=None) -> DiscreteMap"},"get":{"doc_sha":"a1097b27b43d6a27","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"get(x, y) or (pos) -> int | Enum"},"histogram":{"doc_sha":"b2d9df4f14aedd51","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"histogram() -> dict[int, int]"},"invert":{"doc_sha":"0f52aecfe57ff499","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"invert() -> DiscreteMap"},"mask":{"doc_sha":"0d9628616f4592f4","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"mask() -> memoryview"},"max":{"doc_sha":"144265dd0da983a8","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"max(other: DiscreteMap, *, pos=None, source_pos=None, size=None) -> DiscreteMap"},"min":{"doc_sha":"ff90ceef60a45959","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"min(other: DiscreteMap, *, pos=None, source_pos=None, size=None) -> DiscreteMap"},"min_max":{"doc_sha":"22018caaf1bdb60e","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"min_max() -> tuple[int, int]"},"multiply":{"doc_sha":"430abcb2b647132f","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"multiply(factor: float, *, pos=None, size=None) -> DiscreteMap"},"set":{"doc_sha":"8439c14e5785f7e4","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"set(x: int, y: int, value: int) -> None"},"size":{"doc_sha":"fc6c37c9d3916c5d","kind":"property","lifecycle":{"since":"0.2.8"},"signature":"Dimensions (width, height) of the map. Read-only."},"subtract":{"doc_sha":"aeae8042663b9973","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"subtract(other: DiscreteMap | int, *, pos=None, source_pos=None, size=None) -> DiscreteMap"},"to_bytes":{"doc_sha":"4d7c4b5939a546bd","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"to_bytes() -> bytes"},"to_heightmap":{"doc_sha":"b0744db8f889b5d9","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"to_heightmap(mapping: dict[int, float] = None) -> HeightMap"}}},"Drawable":{"doc_sha":"bb28a8d677f24b33","kind":"class","lifecycle":{"since":"0.2.8"},"members":{"move":{"doc_sha":"4a8f9ca3692c6b58","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"move(dx, dy) or (delta) -> None"},"on_click":{"doc_sha":"b3b8de4e294d71b6","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"opacity":{"doc_sha":"c1a7ef41e074519e","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"resize":{"doc_sha":"980bdc9065fc077b","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"resize(width, height) or (size) -> None"},"visible":{"doc_sha":"49221a4ae830f3b8","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"z_index":{"doc_sha":"328d87ae44d9fb71","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""}}},"Easing":{"doc_sha":"e3b0c44298fc1c14","kind":"class","lifecycle":{"since":"0.2.8"},"members":{"as_integer_ratio":{"doc_sha":"3eb115fab0d0e0ec","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"($self, /)"},"bit_count":{"doc_sha":"f276da700b5a2e37","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"($self, /)"},"bit_length":{"doc_sha":"15b29db62fd0fa6c","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"($self, /)"},"conjugate":{"doc_sha":"6517a002faad7c75","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"($self, /)"},"denominator":{"doc_sha":"5051eb1570768851","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"from_bytes":{"doc_sha":"64672607865643a7","kind":"classmethod","lifecycle":{"since":"0.2.8"},"signature":"($type, /, bytes, byteorder='big', *, signed=False)"},"imag":{"doc_sha":"ef78ea410df39b26","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"is_integer":{"doc_sha":"6176763fbac2fd22","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"($self, /)"},"numerator":{"doc_sha":"ed255748ce2eda7c","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"real":{"doc_sha":"d44497f1990af6fe","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"to_bytes":{"doc_sha":"b452319e48e64764","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"($self, /, length=1, byteorder='big', *, signed=False)"}}},"Entity":{"doc_sha":"7231aaf2e3b107d0","kind":"class","lifecycle":{"since":"0.2.8"},"members":{"add_label":{"doc_sha":"be06d30bbd5a41ca","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"add_label(label: str) -> None"},"animate":{"doc_sha":"a06820cecef57a9a","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"animate(property: str, target: Any, duration: float, easing=None, delta=False, loop=False, callback=None, conflict_mode='replace') -> Animation"},"at":{"doc_sha":"b5775513388a1464","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"at(x: int, y: int) -> GridPoint | None"},"behavior_type":{"doc_sha":"745936b7029480fd","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"cell_pos":{"doc_sha":"f8feaba9f3652ca0","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"cell_x":{"doc_sha":"8bdc1199bfb15967","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"cell_y":{"doc_sha":"8fd78a55ede4b259","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"default_behavior":{"doc_sha":"f0a1ea99426408dd","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"die":{"doc_sha":"1369fecbc5a1862a","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"die() -> None"},"draw_pos":{"doc_sha":"2769bf67eec13406","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"find_path":{"doc_sha":"1a672b91a00eb597","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"find_path(target, diagonal_cost: float = 1.41, collide: str = None) -> AStarPath | None"},"grid":{"doc_sha":"d02c7b8c3beb5662","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"grid_pos":{"doc_sha":"d2e9f06a9d9f0a3c","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"grid_x":{"doc_sha":"4108b9ec31aff2c0","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"grid_y":{"doc_sha":"26f7190f34a208b2","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"has_label":{"doc_sha":"0e65dc586b3c8d18","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"has_label(label: str) -> bool"},"index":{"doc_sha":"74e24820bb380594","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"index() -> int"},"labels":{"doc_sha":"4e16ef3312d5b4e7","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"move":{"doc_sha":"5c74f429970a2d2b","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"move(dx, dy) or (delta) -> None"},"move_speed":{"doc_sha":"a3b3867a6d3b830a","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"name":{"doc_sha":"708f75f696069d69","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"opacity":{"doc_sha":"6caae59e941bebf1","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"path_to":{"doc_sha":"5b11e33c92caac2b","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"path_to(x: int, y: int) -> list"},"perspective_map":{"doc_sha":"f61894673d8c27a7","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"pos":{"doc_sha":"172ff9a95e621eaf","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"remove_label":{"doc_sha":"a4a541d7e9cf7275","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"remove_label(label: str) -> None"},"resize":{"doc_sha":"005a04f1d129ebe5","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"resize(width, height) or (size) -> None"},"set_behavior":{"doc_sha":"328826510c72f049","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"set_behavior(type, waypoints=None, turns: int = 0, path=None, pathfinder=None) -> None"},"shader":{"doc_sha":"04503c544358f827","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"sight_radius":{"doc_sha":"4a9ec6c3788b093a","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"sprite_grid":{"doc_sha":"a5ff2c3e5190bc41","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"sprite_index":{"doc_sha":"ca285ed1c6f460df","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"sprite_offset":{"doc_sha":"a92dae4a2555fa7b","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"sprite_offset_x":{"doc_sha":"7ba6504a9bbbf681","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"sprite_offset_y":{"doc_sha":"24d4546e182bb2c8","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"step":{"doc_sha":"8877861183e50020","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"target_label":{"doc_sha":"85acb11a7e713a41","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"texture":{"doc_sha":"1588a0d4779fd069","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"tile_height":{"doc_sha":"7f2ef7cb6d425730","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"tile_size":{"doc_sha":"1a647ababfde2daa","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"tile_width":{"doc_sha":"5d72ddf874e9db54","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"turn_order":{"doc_sha":"eba665bc9c7e75ee","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"uniforms":{"doc_sha":"d302457c5aaa495e","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"update_visibility":{"doc_sha":"d096cd998ed99b49","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"update_visibility() -> None"},"visible":{"doc_sha":"2bca3a265b44b270","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"visible_entities":{"doc_sha":"2482fbbecbfac7ea","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"visible_entities(fov=None, radius: int | None = None) -> list[Entity]"},"x":{"doc_sha":"4ba6a677ea35e7ba","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"y":{"doc_sha":"52b85f7dc8a7cb27","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""}}},"Entity3D":{"doc_sha":"0daee46cf6343e9c","kind":"class","lifecycle":{"since":"0.2.8"},"members":{"anim_clip":{"doc_sha":"f69294fc08fc1d6c","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"anim_frame":{"doc_sha":"41bdd9b6f4407d16","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"anim_loop":{"doc_sha":"b267a372c1aef2cd","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"anim_paused":{"doc_sha":"e100f0be03f09b09","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"anim_speed":{"doc_sha":"33c581c9533b5169","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"anim_time":{"doc_sha":"6b811c83dd2a2184","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"animate":{"doc_sha":"fcf44af080ce16a7","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"animate(property, target, duration, easing=None, delta=False, callback=None, conflict_mode=None)"},"at":{"doc_sha":"69d6101c79794f90","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"at(x, z) -> dict"},"auto_animate":{"doc_sha":"867ca7a9c2b5a4ab","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"clear_path":{"doc_sha":"989bb0a431b58e17","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"clear_path()"},"color":{"doc_sha":"ea9ab21659b6e392","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"follow_path":{"doc_sha":"f514b7092f561550","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"follow_path(path)"},"grid_pos":{"doc_sha":"f20411521f420193","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"idle_clip":{"doc_sha":"855c97611053652c","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"is_moving":{"doc_sha":"b31c1090228993e4","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"model":{"doc_sha":"26ae36cf867d07e9","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"name":{"doc_sha":"acea3a6577d65fbc","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"on_anim_complete":{"doc_sha":"5cc47894576d983d","kind":"property","lifecycle":{"since":"0.2.8"},"signature":"Callback(entity, clip_name) when non-looping animation ends."},"path_to":{"doc_sha":"d6fe3936cc8e2d84","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"path_to(x, z) or path_to(pos=(x, z)) -> list"},"pos":{"doc_sha":"2488492030e9df40","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"rotation":{"doc_sha":"5b603e8c652baf50","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"scale":{"doc_sha":"b06782214d92e3f7","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"teleport":{"doc_sha":"d53364830cdc737a","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"teleport(x, z) or teleport(pos=(x, z))"},"update_visibility":{"doc_sha":"61886fb70daedf6b","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"update_visibility()"},"viewport":{"doc_sha":"e9f9154f00b47f9a","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"visible":{"doc_sha":"bc1ae0f4c6b5cb11","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"walk_clip":{"doc_sha":"c95d11b4e1901f28","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"world_pos":{"doc_sha":"bcc6673be9243dd5","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""}}},"EntityCollection3D":{"doc_sha":"55864ac691527ca9","kind":"class","lifecycle":{"since":"0.2.8"},"members":{"append":{"doc_sha":"43922f93aff7d723","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"append(entity)"},"clear":{"doc_sha":"f44ea077b10186d7","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"clear()"},"extend":{"doc_sha":"4fd2e7e83437c7ae","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"extend(iterable)"},"find":{"doc_sha":"e4182a3a3317aaae","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"find(name) -> Entity3D or None"},"pop":{"doc_sha":"cbcc90e2a433f277","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"pop(index=-1) -> Entity3D"},"remove":{"doc_sha":"b2f8fb276b91ec1e","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"remove(entity)"}}},"EntityCollection3DIter":{"doc_sha":"d71ec48b224bfacc","kind":"class","lifecycle":{"since":"0.2.8"},"members":{}},"FOV":{"doc_sha":"e3b0c44298fc1c14","kind":"class","lifecycle":{"since":"0.2.8"},"members":{"as_integer_ratio":{"doc_sha":"3eb115fab0d0e0ec","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"($self, /)"},"bit_count":{"doc_sha":"f276da700b5a2e37","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"($self, /)"},"bit_length":{"doc_sha":"15b29db62fd0fa6c","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"($self, /)"},"conjugate":{"doc_sha":"6517a002faad7c75","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"($self, /)"},"denominator":{"doc_sha":"5051eb1570768851","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"from_bytes":{"doc_sha":"64672607865643a7","kind":"classmethod","lifecycle":{"since":"0.2.8"},"signature":"($type, /, bytes, byteorder='big', *, signed=False)"},"imag":{"doc_sha":"ef78ea410df39b26","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"is_integer":{"doc_sha":"6176763fbac2fd22","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"($self, /)"},"numerator":{"doc_sha":"ed255748ce2eda7c","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"real":{"doc_sha":"d44497f1990af6fe","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"to_bytes":{"doc_sha":"b452319e48e64764","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"($self, /, length=1, byteorder='big', *, signed=False)"}}},"Font":{"doc_sha":"245b4e9b0ca66fc9","kind":"class","lifecycle":{"since":"0.2.8"},"members":{"family":{"doc_sha":"0cf26d3038432764","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"source":{"doc_sha":"efc970576a584cde","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""}}},"Frame":{"doc_sha":"4d53c3cb9e9e4611","kind":"class","lifecycle":{"since":"0.2.8"},"members":{"align":{"doc_sha":"58657c3cfa010f31","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"animate":{"doc_sha":"400df4f5893261c5","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"animate(property: str, target: Any, duration: float, easing=None, delta=False, loop=False, callback=None, conflict_mode='replace') -> Animation"},"bounds":{"doc_sha":"d3186ac3b3ff8bed","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"cache_subtree":{"doc_sha":"0b8d1175192c1ce2","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"children":{"doc_sha":"b2cf42011b86727c","kind":"property","lifecycle":{"modified":{"commit":"4c0953c","version":"0.2.8-dev"},"since":"0.2.8"},"signature":""},"clip_children":{"doc_sha":"d12b2c4d1d2bb6cb","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"fill_color":{"doc_sha":"8cfbe5aa6dd6da27","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"global_bounds":{"doc_sha":"aa4de9a20df55c2b","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"global_position":{"doc_sha":"b8dabbecc9fccd2a","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"grid_pos":{"doc_sha":"2f5b05882c4ad39f","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"grid_size":{"doc_sha":"9c8707ad589e5d8a","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"h":{"doc_sha":"ced8010da828cc93","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"horiz_margin":{"doc_sha":"0ef227a4cae6e675","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"hovered":{"doc_sha":"af0d353ce08f7be0","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"margin":{"doc_sha":"ab774a7da8cfe9bf","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"move":{"doc_sha":"5c74f429970a2d2b","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"move(dx, dy) or (delta) -> None"},"name":{"doc_sha":"90a2ab0ec07c00eb","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"on_click":{"doc_sha":"b3b8de4e294d71b6","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"on_enter":{"doc_sha":"48c996802e262396","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"on_exit":{"doc_sha":"c14ef0d6d6d40b0e","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"on_move":{"doc_sha":"9c3e26818b5b25b0","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"opacity":{"doc_sha":"c1a7ef41e074519e","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"origin":{"doc_sha":"270bdf366c74bb1d","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"outline":{"doc_sha":"c07b6dec9da5b9ea","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"outline_color":{"doc_sha":"cd98fcaf39f0bc56","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"parent":{"doc_sha":"171b0de4e295c55b","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"pos":{"doc_sha":"55a118e7889fd555","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"realign":{"doc_sha":"8dfa7333a49aa697","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"realign() -> None"},"resize":{"doc_sha":"005a04f1d129ebe5","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"resize(width, height) or (size) -> None"},"rotate_with_camera":{"doc_sha":"5c3af880218d3a08","kind":"property","lifecycle":{"modified":{"commit":"4bb1966","version":"0.2.8-dev"},"since":"0.2.8"},"signature":""},"rotation":{"doc_sha":"61ba3e66a0841a08","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"shader":{"doc_sha":"418952064319ffb5","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"uniforms":{"doc_sha":"b1f2699da618d002","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"vert_margin":{"doc_sha":"e2042be923bc6fcb","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"visible":{"doc_sha":"49221a4ae830f3b8","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"w":{"doc_sha":"bd51299002a099c5","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"x":{"doc_sha":"4ed17c625f2289e3","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"y":{"doc_sha":"fabaa45fd789c7fd","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"z_index":{"doc_sha":"328d87ae44d9fb71","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""}}},"Grid":{"doc_sha":"f88e43e992d43a86","kind":"class","lifecycle":{"since":"0.2.8"},"members":{"align":{"doc_sha":"58657c3cfa010f31","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"animate":{"doc_sha":"400df4f5893261c5","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"animate(property: str, target: Any, duration: float, easing=None, delta=False, loop=False, callback=None, conflict_mode='replace') -> Animation"},"bounds":{"doc_sha":"d3186ac3b3ff8bed","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"camera_rotation":{"doc_sha":"bc2cd502f52817e9","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"center":{"doc_sha":"913ef5c36eaf9635","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"center_camera":{"doc_sha":"00d37ccdedcd3a2e","kind":"method","lifecycle":{"since":"0.2.8-dev"},"signature":"center_camera(pos: tuple = None) -> None"},"center_x":{"doc_sha":"fbb0ff08116f3c80","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"center_y":{"doc_sha":"1bda27633025fb12","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"children":{"doc_sha":"9566d92d5619ee21","kind":"property","lifecycle":{"since":"0.2.8-dev"},"signature":""},"fill_color":{"doc_sha":"f26fb5da3de1789a","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"global_bounds":{"doc_sha":"aa4de9a20df55c2b","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"global_position":{"doc_sha":"b8dabbecc9fccd2a","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"grid_data":{"doc_sha":"fdf0084b8456fd45","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"h":{"doc_sha":"bd5a06618a50efad","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"horiz_margin":{"doc_sha":"0ef227a4cae6e675","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"hovered":{"doc_sha":"af0d353ce08f7be0","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"hovered_cell":{"doc_sha":"85c87c8aa013d0c4","kind":"property","lifecycle":{"since":"0.2.8-dev"},"signature":""},"margin":{"doc_sha":"ab774a7da8cfe9bf","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"move":{"doc_sha":"5c74f429970a2d2b","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"move(dx, dy) or (delta) -> None"},"name":{"doc_sha":"90a2ab0ec07c00eb","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"on_cell_click":{"doc_sha":"ada11ea920c71f29","kind":"property","lifecycle":{"since":"0.2.8-dev"},"signature":""},"on_cell_enter":{"doc_sha":"6ae7882d4f14f183","kind":"property","lifecycle":{"since":"0.2.8-dev"},"signature":""},"on_cell_exit":{"doc_sha":"729303b3a70b2a36","kind":"property","lifecycle":{"since":"0.2.8-dev"},"signature":""},"on_click":{"doc_sha":"4e33bd16b2dee20a","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"on_enter":{"doc_sha":"48c996802e262396","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"on_exit":{"doc_sha":"c14ef0d6d6d40b0e","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"on_move":{"doc_sha":"9c3e26818b5b25b0","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"opacity":{"doc_sha":"c1a7ef41e074519e","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"origin":{"doc_sha":"270bdf366c74bb1d","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"parent":{"doc_sha":"171b0de4e295c55b","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"perspective":{"doc_sha":"8854eef3677c4423","kind":"property","lifecycle":{"since":"0.2.8-dev"},"signature":""},"perspective_enabled":{"doc_sha":"abd2223cd551e1ca","kind":"property","lifecycle":{"since":"0.2.8-dev"},"signature":""},"pos":{"doc_sha":"7f556820c51df2c4","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"realign":{"doc_sha":"8dfa7333a49aa697","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"realign() -> None"},"resize":{"doc_sha":"005a04f1d129ebe5","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"resize(width, height) or (size) -> None"},"rotate_with_camera":{"doc_sha":"5c3af880218d3a08","kind":"property","lifecycle":{"modified":{"commit":"4bb1966","version":"0.2.8-dev"},"since":"0.2.8"},"signature":""},"rotation":{"doc_sha":"61ba3e66a0841a08","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"shader":{"doc_sha":"418952064319ffb5","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"size":{"doc_sha":"cfff2bccd482c2ef","kind":"property","lifecycle":{"since":"0.2.8-dev"},"signature":""},"texture":{"doc_sha":"b9bfb54e5571bfc9","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"uniforms":{"doc_sha":"b1f2699da618d002","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"vert_margin":{"doc_sha":"e2042be923bc6fcb","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"visible":{"doc_sha":"49221a4ae830f3b8","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"w":{"doc_sha":"ec55a5a578c4d993","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"x":{"doc_sha":"2449b0f04153753c","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"y":{"doc_sha":"7958c0625706f2d2","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"z_index":{"doc_sha":"0cb3df5278d2658b","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"zoom":{"doc_sha":"e56c4b61e0303f09","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""}}},"GridData":{"doc_sha":"793d86e4e51839f6","kind":"class","lifecycle":{"since":"0.2.8-dev"},"members":{"add_layer":{"doc_sha":"ad48373d656e6a88","kind":"method","lifecycle":{"since":"0.2.8-dev"},"signature":"add_layer(layer: ColorLayer | TileLayer) -> ColorLayer | TileLayer"},"apply_ranges":{"doc_sha":"f74ca906e65a4bec","kind":"method","lifecycle":{"since":"0.2.8-dev"},"signature":"apply_ranges(source: HeightMap, ranges: list) -> GridData"},"apply_threshold":{"doc_sha":"823c3779c6610389","kind":"method","lifecycle":{"since":"0.2.8-dev"},"signature":"apply_threshold(source: HeightMap, range: tuple, walkable: bool = None, transparent: bool = None) -> GridData"},"at":{"doc_sha":"0f12b1f70eaec14b","kind":"method","lifecycle":{"since":"0.2.8-dev"},"signature":"at(x: int, y: int) -> GridPoint"},"clear_dijkstra_maps":{"doc_sha":"d470811b59885b93","kind":"method","lifecycle":{"since":"0.2.8-dev"},"signature":"clear_dijkstra_maps() -> None"},"compute_fov":{"doc_sha":"2522197d48d43d98","kind":"method","lifecycle":{"since":"0.2.8-dev"},"signature":"compute_fov(pos, radius: int = 0, light_walls: bool = True, algorithm: FOV | int = FOV.BASIC) -> None"},"entities":{"doc_sha":"5c361e2150645749","kind":"property","lifecycle":{"since":"0.2.8-dev"},"signature":""},"entities_in_radius":{"doc_sha":"820857d7995c8fe6","kind":"method","lifecycle":{"since":"0.2.8-dev"},"signature":"entities_in_radius(pos: tuple | Vector, radius: float) -> list"},"find_path":{"doc_sha":"3de3f1a19ba24df8","kind":"method","lifecycle":{"since":"0.2.8-dev"},"signature":"find_path(start, end, diagonal_cost: float = 1.41, collide: str = None, heuristic = None, weight: float = 1.0) -> AStarPath | None"},"fov":{"doc_sha":"bb929647d575d246","kind":"property","lifecycle":{"since":"0.2.8-dev"},"signature":""},"fov_radius":{"doc_sha":"bf76a3199e6d7f0b","kind":"property","lifecycle":{"since":"0.2.8-dev"},"signature":""},"get_dijkstra_map":{"doc_sha":"af44def04f5c942e","kind":"method","lifecycle":{"since":"0.2.8-dev"},"signature":"get_dijkstra_map(root=None, diagonal_cost: float = 1.41, collide: str = None, roots=None) -> DijkstraMap"},"grid_h":{"doc_sha":"f96615d57a28b090","kind":"property","lifecycle":{"since":"0.2.8-dev"},"signature":""},"grid_size":{"doc_sha":"f1aff956f432c1fe","kind":"property","lifecycle":{"since":"0.2.8-dev"},"signature":""},"grid_w":{"doc_sha":"e2f7f55bad731ce6","kind":"property","lifecycle":{"since":"0.2.8-dev"},"signature":""},"is_in_fov":{"doc_sha":"3f871a007bbff04c","kind":"method","lifecycle":{"since":"0.2.8-dev"},"signature":"is_in_fov(x: int, y: int) -> bool"},"layer":{"doc_sha":"38d9c25b2b0b172b","kind":"method","lifecycle":{"since":"0.2.8-dev"},"signature":"layer(name: str) -> ColorLayer | TileLayer | None"},"layers":{"doc_sha":"9d6207bfc4909360","kind":"property","lifecycle":{"since":"0.2.8-dev"},"signature":""},"remove_layer":{"doc_sha":"7876426544d2ec0d","kind":"method","lifecycle":{"since":"0.2.8-dev"},"signature":"remove_layer(name_or_layer: str | ColorLayer | TileLayer) -> None"},"step":{"doc_sha":"38146d2fc84d70f3","kind":"method","lifecycle":{"since":"0.2.8-dev"},"signature":"step(n: int = 1, turn_order: int = None) -> None"},"texture":{"doc_sha":"a2911dd21b916bc0","kind":"property","lifecycle":{"since":"0.2.8-dev"},"signature":""}}},"GridView":{"doc_sha":"f88e43e992d43a86","kind":"class","lifecycle":{"since":"0.2.8"},"members":{"align":{"doc_sha":"58657c3cfa010f31","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"animate":{"doc_sha":"400df4f5893261c5","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"animate(property: str, target: Any, duration: float, easing=None, delta=False, loop=False, callback=None, conflict_mode='replace') -> Animation"},"bounds":{"doc_sha":"d3186ac3b3ff8bed","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"camera_rotation":{"doc_sha":"bc2cd502f52817e9","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"center":{"doc_sha":"913ef5c36eaf9635","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"center_camera":{"doc_sha":"00d37ccdedcd3a2e","kind":"method","lifecycle":{"since":"0.2.8-dev"},"signature":"center_camera(pos: tuple = None) -> None"},"center_x":{"doc_sha":"fbb0ff08116f3c80","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"center_y":{"doc_sha":"1bda27633025fb12","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"children":{"doc_sha":"9566d92d5619ee21","kind":"property","lifecycle":{"since":"0.2.8-dev"},"signature":""},"fill_color":{"doc_sha":"f26fb5da3de1789a","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"global_bounds":{"doc_sha":"aa4de9a20df55c2b","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"global_position":{"doc_sha":"b8dabbecc9fccd2a","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"grid_data":{"doc_sha":"fdf0084b8456fd45","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"h":{"doc_sha":"bd5a06618a50efad","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"horiz_margin":{"doc_sha":"0ef227a4cae6e675","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"hovered":{"doc_sha":"af0d353ce08f7be0","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"hovered_cell":{"doc_sha":"85c87c8aa013d0c4","kind":"property","lifecycle":{"since":"0.2.8-dev"},"signature":""},"margin":{"doc_sha":"ab774a7da8cfe9bf","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"move":{"doc_sha":"5c74f429970a2d2b","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"move(dx, dy) or (delta) -> None"},"name":{"doc_sha":"90a2ab0ec07c00eb","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"on_cell_click":{"doc_sha":"ada11ea920c71f29","kind":"property","lifecycle":{"since":"0.2.8-dev"},"signature":""},"on_cell_enter":{"doc_sha":"6ae7882d4f14f183","kind":"property","lifecycle":{"since":"0.2.8-dev"},"signature":""},"on_cell_exit":{"doc_sha":"729303b3a70b2a36","kind":"property","lifecycle":{"since":"0.2.8-dev"},"signature":""},"on_click":{"doc_sha":"4e33bd16b2dee20a","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"on_enter":{"doc_sha":"48c996802e262396","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"on_exit":{"doc_sha":"c14ef0d6d6d40b0e","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"on_move":{"doc_sha":"9c3e26818b5b25b0","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"opacity":{"doc_sha":"c1a7ef41e074519e","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"origin":{"doc_sha":"270bdf366c74bb1d","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"parent":{"doc_sha":"171b0de4e295c55b","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"perspective":{"doc_sha":"8854eef3677c4423","kind":"property","lifecycle":{"since":"0.2.8-dev"},"signature":""},"perspective_enabled":{"doc_sha":"abd2223cd551e1ca","kind":"property","lifecycle":{"since":"0.2.8-dev"},"signature":""},"pos":{"doc_sha":"7f556820c51df2c4","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"realign":{"doc_sha":"8dfa7333a49aa697","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"realign() -> None"},"resize":{"doc_sha":"005a04f1d129ebe5","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"resize(width, height) or (size) -> None"},"rotate_with_camera":{"doc_sha":"5c3af880218d3a08","kind":"property","lifecycle":{"modified":{"commit":"4bb1966","version":"0.2.8-dev"},"since":"0.2.8"},"signature":""},"rotation":{"doc_sha":"61ba3e66a0841a08","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"shader":{"doc_sha":"418952064319ffb5","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"size":{"doc_sha":"cfff2bccd482c2ef","kind":"property","lifecycle":{"since":"0.2.8-dev"},"signature":""},"texture":{"doc_sha":"b9bfb54e5571bfc9","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"uniforms":{"doc_sha":"b1f2699da618d002","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"vert_margin":{"doc_sha":"e2042be923bc6fcb","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"visible":{"doc_sha":"49221a4ae830f3b8","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"w":{"doc_sha":"ec55a5a578c4d993","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"x":{"doc_sha":"2449b0f04153753c","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"y":{"doc_sha":"7958c0625706f2d2","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"z_index":{"doc_sha":"0cb3df5278d2658b","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"zoom":{"doc_sha":"e56c4b61e0303f09","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""}}},"HeightMap":{"doc_sha":"e5f86ba9c89f2a5a","kind":"class","lifecycle":{"since":"0.2.8"},"members":{"add":{"doc_sha":"d420dd760d7228af","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"add(other: HeightMap, *, pos=None, source_pos=None, size=None) -> HeightMap"},"add_bsp":{"doc_sha":"b70f06e432c91a7a","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"add_bsp(bsp: BSP, *, pos=None, select: str = 'leaves', nodes: list = None, shrink: int = 0, value: float = 1.0) -> HeightMap"},"add_constant":{"doc_sha":"f382eb3f3e667cb4","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"add_constant(value: float, *, pos=None, size=None) -> HeightMap"},"add_hill":{"doc_sha":"1ad2406960701682","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"add_hill(center, radius: float, height: float) -> HeightMap"},"add_noise":{"doc_sha":"3c3c15026ff43959","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"add_noise(source: NoiseSource, world_origin: tuple = (0.0, 0.0), world_size: tuple = None, mode: str = 'fbm', octaves: int = 4, scale: float = 1.0) -> HeightMap"},"add_voronoi":{"doc_sha":"b689af51f6867b78","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"add_voronoi(num_points: int, coefficients: tuple = (1.0, -0.5), seed: int = None) -> HeightMap"},"clamp":{"doc_sha":"60c08680fb281402","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"clamp(min: float = 0.0, max: float = 1.0, *, pos=None, size=None) -> HeightMap"},"clear":{"doc_sha":"2297d98f231b6b7b","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"clear() -> HeightMap"},"copy_from":{"doc_sha":"80d64f3f6e76f8a7","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"copy_from(other: HeightMap, *, pos=None, source_pos=None, size=None) -> HeightMap"},"count_in_range":{"doc_sha":"7af3d60f28d614b6","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"count_in_range(range: tuple[float, float]) -> int"},"dig_bezier":{"doc_sha":"f3327d6ddc165496","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"dig_bezier(points: tuple, start_radius: float, end_radius: float, start_height: float, end_height: float) -> HeightMap"},"dig_hill":{"doc_sha":"be55e494c4e50c3a","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"dig_hill(center, radius: float, target_height: float) -> HeightMap"},"fill":{"doc_sha":"4e998a0b1d85c4f2","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"fill(value: float, *, pos=None, size=None) -> HeightMap"},"get":{"doc_sha":"f55fcc655d43dc50","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"get(x, y) or (pos) -> float"},"get_interpolated":{"doc_sha":"5c3e0267d9cbeda2","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"get_interpolated(x, y) or (pos) -> float"},"get_normal":{"doc_sha":"6d4212add4e533f2","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"get_normal(x, y, water_level=0.0) or (pos, water_level=0.0) -> tuple[float, float, float]"},"get_slope":{"doc_sha":"4876967f738c7d67","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"get_slope(x, y) or (pos) -> float"},"inverse":{"doc_sha":"f6c4de6cd6a9ca0f","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"inverse() -> HeightMap"},"lerp":{"doc_sha":"967cacc180df98bc","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"lerp(other: HeightMap, t: float, *, pos=None, source_pos=None, size=None) -> HeightMap"},"max":{"doc_sha":"141515f8b1f23390","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"max(other: HeightMap, *, pos=None, source_pos=None, size=None) -> HeightMap"},"mid_point_displacement":{"doc_sha":"bfcbfe796d73410f","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"mid_point_displacement(roughness: float = 0.5, seed: int = None) -> HeightMap"},"min":{"doc_sha":"c51e934ea5047d91","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"min(other: HeightMap, *, pos=None, source_pos=None, size=None) -> HeightMap"},"min_max":{"doc_sha":"4e0b3cb7472a7cfe","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"min_max() -> tuple[float, float]"},"multiply":{"doc_sha":"83d2fcea22463d18","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"multiply(other: HeightMap, *, pos=None, source_pos=None, size=None) -> HeightMap"},"multiply_bsp":{"doc_sha":"21f401c651344592","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"multiply_bsp(bsp: BSP, *, pos=None, select: str = 'leaves', nodes: list = None, shrink: int = 0, value: float = 1.0) -> HeightMap"},"multiply_noise":{"doc_sha":"177d1d06a401af54","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"multiply_noise(source: NoiseSource, world_origin: tuple = (0.0, 0.0), world_size: tuple = None, mode: str = 'fbm', octaves: int = 4, scale: float = 1.0) -> HeightMap"},"normalize":{"doc_sha":"93ce05bf80d158ed","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"normalize(min: float = 0.0, max: float = 1.0, *, pos=None, size=None) -> HeightMap"},"rain_erosion":{"doc_sha":"4dbe6bd42d9f6e5b","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"rain_erosion(drops: int, erosion: float = 0.1, sedimentation: float = 0.05, seed: int = None) -> HeightMap"},"scale":{"doc_sha":"c85d9324b84c43ff","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"scale(factor: float, *, pos=None, size=None) -> HeightMap"},"size":{"doc_sha":"31846e24f4028b35","kind":"property","lifecycle":{"since":"0.2.8"},"signature":"Dimensions (width, height) of the heightmap. Read-only."},"smooth":{"doc_sha":"243be30aa73da43b","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"smooth(iterations: int = 1) -> HeightMap"},"sparse_kernel":{"doc_sha":"e3b28d2c3913ac74","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"sparse_kernel(weights: dict[tuple[int, int], float]) -> HeightMap"},"sparse_kernel_from":{"doc_sha":"ebeed84050ee0b2f","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"sparse_kernel_from(source: HeightMap, weights: dict[tuple[int, int], float]) -> None"},"subtract":{"doc_sha":"8e209f74aaad794d","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"subtract(other: HeightMap, *, pos=None, source_pos=None, size=None) -> HeightMap"},"threshold":{"doc_sha":"946f6a1fb850decb","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"threshold(range: tuple[float, float]) -> HeightMap"},"threshold_binary":{"doc_sha":"6ebe7bd7cbc112c2","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"threshold_binary(range: tuple[float, float], value: float = 1.0) -> HeightMap"}}},"Heuristic":{"doc_sha":"7582db951ab67fc5","kind":"class","lifecycle":{"since":"0.2.8"},"members":{"as_integer_ratio":{"doc_sha":"3eb115fab0d0e0ec","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"($self, /)"},"bit_count":{"doc_sha":"f276da700b5a2e37","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"($self, /)"},"bit_length":{"doc_sha":"15b29db62fd0fa6c","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"($self, /)"},"conjugate":{"doc_sha":"6517a002faad7c75","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"($self, /)"},"denominator":{"doc_sha":"5051eb1570768851","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"from_bytes":{"doc_sha":"64672607865643a7","kind":"classmethod","lifecycle":{"since":"0.2.8"},"signature":"($type, /, bytes, byteorder='big', *, signed=False)"},"imag":{"doc_sha":"ef78ea410df39b26","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"is_integer":{"doc_sha":"6176763fbac2fd22","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"($self, /)"},"numerator":{"doc_sha":"ed255748ce2eda7c","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"real":{"doc_sha":"d44497f1990af6fe","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"to_bytes":{"doc_sha":"b452319e48e64764","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"($self, /, length=1, byteorder='big', *, signed=False)"}}},"InputState":{"doc_sha":"c7b7558edda29fea","kind":"class","lifecycle":{"since":"0.2.8"},"members":{"as_integer_ratio":{"doc_sha":"3eb115fab0d0e0ec","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"($self, /)"},"bit_count":{"doc_sha":"f276da700b5a2e37","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"($self, /)"},"bit_length":{"doc_sha":"15b29db62fd0fa6c","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"($self, /)"},"conjugate":{"doc_sha":"6517a002faad7c75","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"($self, /)"},"denominator":{"doc_sha":"5051eb1570768851","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"from_bytes":{"doc_sha":"64672607865643a7","kind":"classmethod","lifecycle":{"since":"0.2.8"},"signature":"($type, /, bytes, byteorder='big', *, signed=False)"},"imag":{"doc_sha":"ef78ea410df39b26","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"is_integer":{"doc_sha":"6176763fbac2fd22","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"($self, /)"},"numerator":{"doc_sha":"ed255748ce2eda7c","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"real":{"doc_sha":"d44497f1990af6fe","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"to_bytes":{"doc_sha":"b452319e48e64764","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"($self, /, length=1, byteorder='big', *, signed=False)"}}},"Key":{"doc_sha":"bc4feb60367e4be7","kind":"class","lifecycle":{"since":"0.2.8"},"members":{"as_integer_ratio":{"doc_sha":"3eb115fab0d0e0ec","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"($self, /)"},"bit_count":{"doc_sha":"f276da700b5a2e37","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"($self, /)"},"bit_length":{"doc_sha":"15b29db62fd0fa6c","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"($self, /)"},"conjugate":{"doc_sha":"6517a002faad7c75","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"($self, /)"},"denominator":{"doc_sha":"5051eb1570768851","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"from_bytes":{"doc_sha":"64672607865643a7","kind":"classmethod","lifecycle":{"since":"0.2.8"},"signature":"($type, /, bytes, byteorder='big', *, signed=False)"},"imag":{"doc_sha":"ef78ea410df39b26","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"is_integer":{"doc_sha":"6176763fbac2fd22","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"($self, /)"},"numerator":{"doc_sha":"ed255748ce2eda7c","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"real":{"doc_sha":"d44497f1990af6fe","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"to_bytes":{"doc_sha":"b452319e48e64764","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"($self, /, length=1, byteorder='big', *, signed=False)"}}},"Keyboard":{"doc_sha":"afbde6bcf7bfe1c6","kind":"class","lifecycle":{"since":"0.2.8"},"members":{"alt":{"doc_sha":"051cb7dbc470d328","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"ctrl":{"doc_sha":"c062912b0b816bd2","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"shift":{"doc_sha":"614beb0d432c2ec0","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"system":{"doc_sha":"2292efb47472f5e7","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""}}},"LdtkProject":{"doc_sha":"d80a5fdda811cb33","kind":"class","lifecycle":{"since":"0.2.8"},"members":{"enums":{"doc_sha":"5697f9e75980e2be","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"level":{"doc_sha":"33b80035be788d03","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"level(name: str) -> dict"},"level_names":{"doc_sha":"9479f6c2bd85608d","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"ruleset":{"doc_sha":"ce72d154e5dd391c","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"ruleset(name: str) -> AutoRuleSet"},"ruleset_names":{"doc_sha":"28983e9702e0dc81","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"tileset":{"doc_sha":"30eabe3003acb244","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"tileset(name: str) -> TileSetFile"},"tileset_names":{"doc_sha":"77a45b51f49743ef","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"version":{"doc_sha":"e6d3d49ad9edfad8","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""}}},"Line":{"doc_sha":"8d778faa93b5d0d5","kind":"class","lifecycle":{"since":"0.2.8"},"members":{"align":{"doc_sha":"58657c3cfa010f31","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"animate":{"doc_sha":"400df4f5893261c5","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"animate(property: str, target: Any, duration: float, easing=None, delta=False, loop=False, callback=None, conflict_mode='replace') -> Animation"},"bounds":{"doc_sha":"d3186ac3b3ff8bed","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"color":{"doc_sha":"f7aca0b82eaae97e","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"end":{"doc_sha":"cc14ff1baa1b61fa","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"global_bounds":{"doc_sha":"aa4de9a20df55c2b","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"global_position":{"doc_sha":"b8dabbecc9fccd2a","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"grid_pos":{"doc_sha":"9df77e9a996208ba","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"grid_size":{"doc_sha":"e32208817299dfe9","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"horiz_margin":{"doc_sha":"0ef227a4cae6e675","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"hovered":{"doc_sha":"af0d353ce08f7be0","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"margin":{"doc_sha":"ab774a7da8cfe9bf","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"move":{"doc_sha":"5c74f429970a2d2b","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"move(dx, dy) or (delta) -> None"},"name":{"doc_sha":"feeb7cb507a4692d","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"on_click":{"doc_sha":"49cecfb458b010a1","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"on_enter":{"doc_sha":"48c996802e262396","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"on_exit":{"doc_sha":"c14ef0d6d6d40b0e","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"on_move":{"doc_sha":"9c3e26818b5b25b0","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"opacity":{"doc_sha":"c1a7ef41e074519e","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"origin":{"doc_sha":"270bdf366c74bb1d","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"parent":{"doc_sha":"171b0de4e295c55b","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"pos":{"doc_sha":"733a48ea3a730e60","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"realign":{"doc_sha":"8dfa7333a49aa697","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"realign() -> None"},"resize":{"doc_sha":"005a04f1d129ebe5","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"resize(width, height) or (size) -> None"},"rotate_with_camera":{"doc_sha":"5c3af880218d3a08","kind":"property","lifecycle":{"modified":{"commit":"4bb1966","version":"0.2.8-dev"},"since":"0.2.8"},"signature":""},"rotation":{"doc_sha":"61ba3e66a0841a08","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"start":{"doc_sha":"817049419558ea17","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"thickness":{"doc_sha":"b6c7f3e979028709","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"vert_margin":{"doc_sha":"e2042be923bc6fcb","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"visible":{"doc_sha":"49221a4ae830f3b8","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"z_index":{"doc_sha":"cecb3704206d35f7","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""}}},"Model3D":{"doc_sha":"be7fb5003ed48d84","kind":"class","lifecycle":{"since":"0.2.8"},"members":{"animation_clips":{"doc_sha":"0d15ad9839919fe5","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"bone_count":{"doc_sha":"6e389fb2496946bf","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"bounds":{"doc_sha":"4ddfb8f6ae5a8928","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"cube":{"doc_sha":"b49160efa8b0f71e","kind":"classmethod","lifecycle":{"since":"0.2.8"},"signature":"cube(size=1.0) -> Model3D"},"has_skeleton":{"doc_sha":"4e9ac073dac101b6","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"mesh_count":{"doc_sha":"49f8fe0ece668d77","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"name":{"doc_sha":"b577d22f9a18de0c","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"plane":{"doc_sha":"5e31b94e295b4429","kind":"classmethod","lifecycle":{"since":"0.2.8"},"signature":"plane(width=1.0, depth=1.0, segments=1) -> Model3D"},"sphere":{"doc_sha":"b385d8aaf0efd485","kind":"classmethod","lifecycle":{"since":"0.2.8"},"signature":"sphere(radius=0.5, segments=16, rings=12) -> Model3D"},"triangle_count":{"doc_sha":"b67c98fac8ffc8dd","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"vertex_count":{"doc_sha":"8b76e70b26a820df","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""}}},"Mouse":{"doc_sha":"e64f561c8a08bdec","kind":"class","lifecycle":{"since":"0.2.8"},"members":{"grabbed":{"doc_sha":"3da420bb9cac434f","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"left":{"doc_sha":"a4c7b98babc093cb","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"middle":{"doc_sha":"1306b1240c6f64e3","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"pos":{"doc_sha":"2ecba0a68d64976a","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"right":{"doc_sha":"08d5534c1c034b39","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"visible":{"doc_sha":"834527eb5579f3e3","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"x":{"doc_sha":"c142f98ee94bf3f7","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"y":{"doc_sha":"5095db9e9e163e92","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""}}},"MouseButton":{"doc_sha":"a5a7584949d18f01","kind":"class","lifecycle":{"since":"0.2.8"},"members":{"as_integer_ratio":{"doc_sha":"3eb115fab0d0e0ec","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"($self, /)"},"bit_count":{"doc_sha":"f276da700b5a2e37","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"($self, /)"},"bit_length":{"doc_sha":"15b29db62fd0fa6c","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"($self, /)"},"conjugate":{"doc_sha":"6517a002faad7c75","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"($self, /)"},"denominator":{"doc_sha":"5051eb1570768851","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"from_bytes":{"doc_sha":"64672607865643a7","kind":"classmethod","lifecycle":{"since":"0.2.8"},"signature":"($type, /, bytes, byteorder='big', *, signed=False)"},"imag":{"doc_sha":"ef78ea410df39b26","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"is_integer":{"doc_sha":"6176763fbac2fd22","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"($self, /)"},"numerator":{"doc_sha":"ed255748ce2eda7c","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"real":{"doc_sha":"d44497f1990af6fe","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"to_bytes":{"doc_sha":"b452319e48e64764","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"($self, /, length=1, byteorder='big', *, signed=False)"}}},"Music":{"doc_sha":"e13e6ad378ec0e91","kind":"class","lifecycle":{"since":"0.2.8"},"members":{"duration":{"doc_sha":"25607c5fa852d501","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"loop":{"doc_sha":"a165a91f8ff771f5","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"pause":{"doc_sha":"4eb6d242151147dc","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"pause() -> None"},"play":{"doc_sha":"f724039e0cdb2847","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"play() -> None"},"playing":{"doc_sha":"4451d007747fa3b1","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"position":{"doc_sha":"608a5f6d4ed927bc","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"source":{"doc_sha":"ee5d7818bae0a255","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"stop":{"doc_sha":"f3289bbb98a4da1f","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"stop() -> None"},"volume":{"doc_sha":"749fc9d89252cb0e","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""}}},"NoiseSource":{"doc_sha":"3bfa3b53984c85cb","kind":"class","lifecycle":{"since":"0.2.8"},"members":{"algorithm":{"doc_sha":"ba813d6c022f15f0","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"dimensions":{"doc_sha":"f534d20928d8d67a","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"fbm":{"doc_sha":"d13c8670fc68b9df","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"fbm(pos: tuple[float, ...], octaves: int = 4) -> float"},"get":{"doc_sha":"3d7776fffd7a0a39","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"get(pos: tuple[float, ...]) -> float"},"hurst":{"doc_sha":"11682e6dc42f2417","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"lacunarity":{"doc_sha":"4d07f7079f3938de","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"sample":{"doc_sha":"63fd0e680000d0f0","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"sample(size: tuple[int, int], world_origin: tuple[float, float] = (0.0, 0.0), world_size: tuple[float, float] = None, mode: str = 'fbm', octaves: int = 4) -> HeightMap"},"seed":{"doc_sha":"17bc1e90a8ce6b23","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"turbulence":{"doc_sha":"800c841f913b3780","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"turbulence(pos: tuple[float, ...], octaves: int = 4) -> float"}}},"Perspective":{"doc_sha":"e1c8553ea2a32a40","kind":"class","lifecycle":{"since":"0.2.8"},"members":{"as_integer_ratio":{"doc_sha":"3eb115fab0d0e0ec","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"($self, /)"},"bit_count":{"doc_sha":"f276da700b5a2e37","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"($self, /)"},"bit_length":{"doc_sha":"15b29db62fd0fa6c","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"($self, /)"},"conjugate":{"doc_sha":"6517a002faad7c75","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"($self, /)"},"denominator":{"doc_sha":"5051eb1570768851","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"from_bytes":{"doc_sha":"64672607865643a7","kind":"classmethod","lifecycle":{"since":"0.2.8"},"signature":"($type, /, bytes, byteorder='big', *, signed=False)"},"imag":{"doc_sha":"ef78ea410df39b26","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"is_integer":{"doc_sha":"6176763fbac2fd22","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"($self, /)"},"numerator":{"doc_sha":"ed255748ce2eda7c","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"real":{"doc_sha":"d44497f1990af6fe","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"to_bytes":{"doc_sha":"b452319e48e64764","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"($self, /, length=1, byteorder='big', *, signed=False)"}}},"PropertyBinding":{"doc_sha":"a23eb43edd46bb66","kind":"class","lifecycle":{"since":"0.2.8"},"members":{"is_valid":{"doc_sha":"40ada14485c362a7","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"property":{"doc_sha":"0dad06f4e661db2d","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"target":{"doc_sha":"3b15e1922fd910c9","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"value":{"doc_sha":"80d28b4e329837d8","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""}}},"Scene":{"doc_sha":"a156c42d77b20a3e","kind":"class","lifecycle":{"since":"0.2.8"},"members":{"activate":{"doc_sha":"54ae3cd3cf652660","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"activate(transition: Transition = None, duration: float = None) -> None"},"active":{"doc_sha":"a59f70d3c12a7cfe","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"children":{"doc_sha":"f21fb480dac856b6","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"name":{"doc_sha":"7feed093cdf9334a","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"on_key":{"doc_sha":"40ece618402a45f3","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"opacity":{"doc_sha":"9e60a4ae9bdb493b","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"pos":{"doc_sha":"50e29b0dd34a76d5","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"realign":{"doc_sha":"eef1e2dce9ee8894","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"realign() -> None"},"register":{"doc_sha":"7603337801a7a498","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"register() -> None"},"registered":{"doc_sha":"d9ff54f488ed5b87","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"unregister":{"doc_sha":"fda6be9891d1f502","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"unregister() -> None"},"visible":{"doc_sha":"a7f321f4013dfb9b","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""}}},"Shader":{"doc_sha":"aded56cb37cba9d8","kind":"class","lifecycle":{"since":"0.2.8"},"members":{"dynamic":{"doc_sha":"43d8bc587c3997e3","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"is_valid":{"doc_sha":"fcdf95eedfa8479e","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"set_uniform":{"doc_sha":"bdaf877e5bc28f21","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"set_uniform(name: str, value: float|tuple) -> None"},"source":{"doc_sha":"dbd462698aadf450","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""}}},"Sound":{"doc_sha":"8f31cca1781580d5","kind":"class","lifecycle":{"since":"0.2.8"},"members":{"buffer":{"doc_sha":"c79f4e433d4300ac","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"duration":{"doc_sha":"48daac900b6eaf36","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"loop":{"doc_sha":"a624dff0e72a5337","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"pause":{"doc_sha":"005627aca21692d3","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"pause() -> None"},"pitch":{"doc_sha":"80b38bfc202b4291","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"play":{"doc_sha":"ae090ee8c0320797","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"play() -> None"},"play_varied":{"doc_sha":"fd9b5cd6db3cb1cf","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"play_varied(pitch_range: float = 0.1, volume_range: float = 3.0) -> None"},"playing":{"doc_sha":"8a0c491fa38061d5","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"source":{"doc_sha":"1736ca7393d96c48","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"stop":{"doc_sha":"f3289bbb98a4da1f","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"stop() -> None"},"volume":{"doc_sha":"749fc9d89252cb0e","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""}}},"SoundBuffer":{"doc_sha":"426618cde68541be","kind":"class","lifecycle":{"since":"0.2.8"},"members":{"bit_crush":{"doc_sha":"66ae55272fd9d182","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"bit_crush(bits: int, rate_divisor: int) -> SoundBuffer"},"channels":{"doc_sha":"f3948c1ceb5d7aec","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"concat":{"doc_sha":"7ee10bfe76aae5e2","kind":"classmethod","lifecycle":{"since":"0.2.8"},"signature":"concat(buffers: list[SoundBuffer], overlap: float = 0.0) -> SoundBuffer"},"distortion":{"doc_sha":"a36442ca725e5cc6","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"distortion(drive: float) -> SoundBuffer"},"duration":{"doc_sha":"bc1c03f781e0372b","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"echo":{"doc_sha":"1b93f79c671068ab","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"echo(delay_ms: float, feedback: float, wet: float) -> SoundBuffer"},"from_samples":{"doc_sha":"556326f1f2cda4a7","kind":"classmethod","lifecycle":{"since":"0.2.8"},"signature":"from_samples(data: bytes, channels: int, sample_rate: int) -> SoundBuffer"},"gain":{"doc_sha":"22fc3aad26b084a5","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"gain(factor: float) -> SoundBuffer"},"high_pass":{"doc_sha":"7d8af12720a4bbae","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"high_pass(cutoff_hz: float) -> SoundBuffer"},"low_pass":{"doc_sha":"82cdc215e7da9310","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"low_pass(cutoff_hz: float) -> SoundBuffer"},"mix":{"doc_sha":"4df14cf741427da9","kind":"classmethod","lifecycle":{"since":"0.2.8"},"signature":"mix(buffers: list[SoundBuffer]) -> SoundBuffer"},"normalize":{"doc_sha":"5476b16208b1ac7b","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"normalize() -> SoundBuffer"},"pitch_shift":{"doc_sha":"ee9373f2d71fdaa8","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"pitch_shift(factor: float) -> SoundBuffer"},"reverb":{"doc_sha":"b6d0dcf1e5f75a42","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"reverb(room_size: float, damping: float, wet: float) -> SoundBuffer"},"reverse":{"doc_sha":"eb70bfa5da4c860d","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"reverse() -> SoundBuffer"},"sample_count":{"doc_sha":"5e96b7263eade6af","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"sample_rate":{"doc_sha":"a41417a71e10a2d6","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"sfxr":{"doc_sha":"5f45a4b7afc74066","kind":"classmethod","lifecycle":{"since":"0.2.8"},"signature":"sfxr(preset: str = None, seed: int = None, **params) -> SoundBuffer"},"sfxr_mutate":{"doc_sha":"68b5b653de4f55dc","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"sfxr_mutate(amount: float = 0.05, seed: int = None) -> SoundBuffer"},"sfxr_params":{"doc_sha":"7fd7a2356600014a","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"slice":{"doc_sha":"84099d0f46e6b7ad","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"slice(start: float, end: float) -> SoundBuffer"},"tone":{"doc_sha":"eaec1748a9089774","kind":"classmethod","lifecycle":{"since":"0.2.8"},"signature":"tone(frequency: float, duration: float, waveform: str = 'sine', ...) -> SoundBuffer"}}},"Sprite":{"doc_sha":"a687c4f393725428","kind":"class","lifecycle":{"since":"0.2.8"},"members":{"align":{"doc_sha":"58657c3cfa010f31","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"animate":{"doc_sha":"400df4f5893261c5","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"animate(property: str, target: Any, duration: float, easing=None, delta=False, loop=False, callback=None, conflict_mode='replace') -> Animation"},"bounds":{"doc_sha":"d3186ac3b3ff8bed","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"global_bounds":{"doc_sha":"aa4de9a20df55c2b","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"global_position":{"doc_sha":"b8dabbecc9fccd2a","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"grid_pos":{"doc_sha":"9df77e9a996208ba","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"grid_size":{"doc_sha":"e32208817299dfe9","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"horiz_margin":{"doc_sha":"0ef227a4cae6e675","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"hovered":{"doc_sha":"af0d353ce08f7be0","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"margin":{"doc_sha":"ab774a7da8cfe9bf","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"move":{"doc_sha":"5c74f429970a2d2b","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"move(dx, dy) or (delta) -> None"},"name":{"doc_sha":"90a2ab0ec07c00eb","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"on_click":{"doc_sha":"b3b8de4e294d71b6","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"on_enter":{"doc_sha":"48c996802e262396","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"on_exit":{"doc_sha":"c14ef0d6d6d40b0e","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"on_move":{"doc_sha":"9c3e26818b5b25b0","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"opacity":{"doc_sha":"c1a7ef41e074519e","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"origin":{"doc_sha":"270bdf366c74bb1d","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"parent":{"doc_sha":"171b0de4e295c55b","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"pos":{"doc_sha":"55a118e7889fd555","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"realign":{"doc_sha":"8dfa7333a49aa697","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"realign() -> None"},"resize":{"doc_sha":"005a04f1d129ebe5","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"resize(width, height) or (size) -> None"},"rotate_with_camera":{"doc_sha":"5c3af880218d3a08","kind":"property","lifecycle":{"modified":{"commit":"4bb1966","version":"0.2.8-dev"},"since":"0.2.8"},"signature":""},"rotation":{"doc_sha":"61ba3e66a0841a08","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"scale":{"doc_sha":"f91a66d322ee1a75","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"scale_x":{"doc_sha":"95f47a809a8ac927","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"scale_y":{"doc_sha":"8567691c22a9b5f8","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"shader":{"doc_sha":"418952064319ffb5","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"sprite_index":{"doc_sha":"826af9fc52ae5cef","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"texture":{"doc_sha":"df3e7671926aa0a3","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"uniforms":{"doc_sha":"b1f2699da618d002","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"vert_margin":{"doc_sha":"e2042be923bc6fcb","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"visible":{"doc_sha":"49221a4ae830f3b8","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"x":{"doc_sha":"528487f8deaaf789","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"y":{"doc_sha":"114ecec940477107","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"z_index":{"doc_sha":"328d87ae44d9fb71","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""}}},"Texture":{"doc_sha":"da368b536d9f844e","kind":"class","lifecycle":{"since":"0.2.8"},"members":{"composite":{"doc_sha":"0bf406582f618af3","kind":"classmethod","lifecycle":{"since":"0.2.8"},"signature":"composite(layers: list[Texture], sprite_width: int, sprite_height: int, name: str = '') -> Texture"},"display_height":{"doc_sha":"0dc1ccea45ff4e40","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"display_offset_x":{"doc_sha":"0ca738fa6559aabb","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"display_offset_y":{"doc_sha":"83b5bfadd26f4e79","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"display_width":{"doc_sha":"cfc6525a8a0b5bb7","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"from_bytes":{"doc_sha":"979f5a384770d87f","kind":"classmethod","lifecycle":{"since":"0.2.8"},"signature":"from_bytes(data: bytes, width: int, height: int, sprite_width: int, sprite_height: int, name: str = '') -> Texture"},"hsl_shift":{"doc_sha":"95f19de7a67646e3","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"hsl_shift(hue_shift: float, sat_shift: float = 0.0, lit_shift: float = 0.0) -> Texture"},"sheet_height":{"doc_sha":"59d4fb71cae72fba","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"sheet_width":{"doc_sha":"54ef54eb9c39a907","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"source":{"doc_sha":"51bee1cdb17257ba","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"sprite_count":{"doc_sha":"d1ed79d8b59eca99","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"sprite_height":{"doc_sha":"19b6a36f45f965e8","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"sprite_width":{"doc_sha":"6f9276522c3b2512","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""}}},"TileLayer":{"doc_sha":"5551e3bf9e668d89","kind":"class","lifecycle":{"since":"0.2.8"},"members":{"apply_ranges":{"doc_sha":"e443e268532fdd2f","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"apply_ranges(source: HeightMap, ranges: list) -> TileLayer"},"apply_threshold":{"doc_sha":"2253f4367656d073","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"apply_threshold(source: HeightMap, range: tuple, tile: int) -> TileLayer"},"at":{"doc_sha":"19975fea19b3fadb","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"at(pos: tuple | Vector) or (x: int, y: int) -> int"},"edit":{"doc_sha":"47c32fda0e88780a","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"edit() -> context manager"},"fill":{"doc_sha":"5c8eca820794a605","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"fill(index: int) -> None"},"fill_rect":{"doc_sha":"adfc5ff6d6e91cc8","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"fill_rect(pos: tuple, size: tuple, index: int) -> None"},"grid":{"doc_sha":"b965c55d57de78fa","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"grid_size":{"doc_sha":"eac88eb0f7b16cbf","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"name":{"doc_sha":"dd282cc9273e2e36","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"set":{"doc_sha":"2b242556ce84080f","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"set(pos, index: int) -> None"},"texture":{"doc_sha":"1d211a219f88c72c","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"visible":{"doc_sha":"ecea81ec16e31c19","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"z_index":{"doc_sha":"eb13c1b4de11ce63","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""}}},"TileMapFile":{"doc_sha":"b26cb3870e41a2c9","kind":"class","lifecycle":{"since":"0.2.8"},"members":{"apply_to_tile_layer":{"doc_sha":"436e4fa972bd79d0","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"apply_to_tile_layer(tile_layer: TileLayer, layer_name: str, tileset_index: int = 0) -> None"},"height":{"doc_sha":"72aa81aefdab3ddb","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"object_layer":{"doc_sha":"e658613bcbbe9b3a","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"object_layer(name: str) -> list[dict]"},"object_layer_names":{"doc_sha":"7af1cc1454cd97f9","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"orientation":{"doc_sha":"2ee759a156b5c903","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"properties":{"doc_sha":"f2cb52eb132891d8","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"resolve_gid":{"doc_sha":"4a72f1c9f61cec27","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"resolve_gid(gid: int) -> tuple[int, int]"},"tile_height":{"doc_sha":"cc47760c48b8c9be","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"tile_layer_data":{"doc_sha":"399fd655e6402442","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"tile_layer_data(name: str) -> list[int]"},"tile_layer_names":{"doc_sha":"e225e2d4b71155e7","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"tile_width":{"doc_sha":"c109808c1e2d3ca1","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"tileset":{"doc_sha":"a3e35f368c3cc89d","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"tileset(index: int) -> tuple[int, TileSetFile]"},"tileset_count":{"doc_sha":"e7347387a4bcfeae","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"width":{"doc_sha":"acef1b7138a85569","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""}}},"TileSetFile":{"doc_sha":"ec83598f02a06258","kind":"class","lifecycle":{"since":"0.2.8"},"members":{"columns":{"doc_sha":"5ba187696ce9596e","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"image_source":{"doc_sha":"f21faacc0392eec3","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"margin":{"doc_sha":"6e1b3cfd5b59af6a","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"name":{"doc_sha":"5e0a76b40f4db151","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"properties":{"doc_sha":"e0382abd62d297a6","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"spacing":{"doc_sha":"950416fc0b8f1837","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"tile_count":{"doc_sha":"df5ccd0141400827","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"tile_height":{"doc_sha":"71a327fd061c1cf2","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"tile_info":{"doc_sha":"b33077f44c79e050","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"tile_info(tile_id: int) -> dict | None"},"tile_width":{"doc_sha":"888787b1ef69f5d9","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"to_texture":{"doc_sha":"610adac758051370","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"to_texture() -> Texture"},"wang_set":{"doc_sha":"c3b15db4611c250a","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"wang_set(name: str) -> WangSet"},"wang_sets":{"doc_sha":"71541a1c5777b29f","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""}}},"Timer":{"doc_sha":"646feff7bdb817f5","kind":"class","lifecycle":{"since":"0.2.8"},"members":{"active":{"doc_sha":"0ac0f4a607819264","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"callback":{"doc_sha":"a943e7e99b75fa22","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"interval":{"doc_sha":"e32658ee97f94823","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"name":{"doc_sha":"98a62482b59640f6","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"once":{"doc_sha":"7d5a7d20bae3793e","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"pause":{"doc_sha":"bfe544cc3c9f1cda","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"pause() -> None"},"paused":{"doc_sha":"060df10cbfc34662","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"remaining":{"doc_sha":"e223a64ed6a1ec63","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"restart":{"doc_sha":"a8c305ae5017cce0","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"restart() -> None"},"resume":{"doc_sha":"66ab0cd39f579ef9","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"resume() -> None"},"start":{"doc_sha":"0b2b51d812995d34","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"start() -> None"},"stop":{"doc_sha":"9f69f5e68512116c","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"stop() -> None"},"stopped":{"doc_sha":"bffdb8aa4589c28b","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""}}},"Transition":{"doc_sha":"e3b0c44298fc1c14","kind":"class","lifecycle":{"since":"0.2.8"},"members":{"as_integer_ratio":{"doc_sha":"3eb115fab0d0e0ec","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"($self, /)"},"bit_count":{"doc_sha":"f276da700b5a2e37","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"($self, /)"},"bit_length":{"doc_sha":"15b29db62fd0fa6c","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"($self, /)"},"conjugate":{"doc_sha":"6517a002faad7c75","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"($self, /)"},"denominator":{"doc_sha":"5051eb1570768851","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"from_bytes":{"doc_sha":"64672607865643a7","kind":"classmethod","lifecycle":{"since":"0.2.8"},"signature":"($type, /, bytes, byteorder='big', *, signed=False)"},"imag":{"doc_sha":"ef78ea410df39b26","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"is_integer":{"doc_sha":"6176763fbac2fd22","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"($self, /)"},"numerator":{"doc_sha":"ed255748ce2eda7c","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"real":{"doc_sha":"d44497f1990af6fe","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"to_bytes":{"doc_sha":"b452319e48e64764","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"($self, /, length=1, byteorder='big', *, signed=False)"}}},"Traversal":{"doc_sha":"e3b0c44298fc1c14","kind":"class","lifecycle":{"since":"0.2.8"},"members":{"as_integer_ratio":{"doc_sha":"3eb115fab0d0e0ec","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"($self, /)"},"bit_count":{"doc_sha":"f276da700b5a2e37","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"($self, /)"},"bit_length":{"doc_sha":"15b29db62fd0fa6c","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"($self, /)"},"conjugate":{"doc_sha":"6517a002faad7c75","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"($self, /)"},"denominator":{"doc_sha":"5051eb1570768851","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"from_bytes":{"doc_sha":"64672607865643a7","kind":"classmethod","lifecycle":{"since":"0.2.8"},"signature":"($type, /, bytes, byteorder='big', *, signed=False)"},"imag":{"doc_sha":"ef78ea410df39b26","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"is_integer":{"doc_sha":"6176763fbac2fd22","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"($self, /)"},"numerator":{"doc_sha":"ed255748ce2eda7c","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"real":{"doc_sha":"d44497f1990af6fe","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"to_bytes":{"doc_sha":"b452319e48e64764","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"($self, /, length=1, byteorder='big', *, signed=False)"}}},"Trigger":{"doc_sha":"3f698ad8b187be9f","kind":"class","lifecycle":{"since":"0.2.8"},"members":{"as_integer_ratio":{"doc_sha":"3eb115fab0d0e0ec","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"($self, /)"},"bit_count":{"doc_sha":"f276da700b5a2e37","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"($self, /)"},"bit_length":{"doc_sha":"15b29db62fd0fa6c","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"($self, /)"},"conjugate":{"doc_sha":"6517a002faad7c75","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"($self, /)"},"denominator":{"doc_sha":"5051eb1570768851","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"from_bytes":{"doc_sha":"64672607865643a7","kind":"classmethod","lifecycle":{"since":"0.2.8"},"signature":"($type, /, bytes, byteorder='big', *, signed=False)"},"imag":{"doc_sha":"ef78ea410df39b26","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"is_integer":{"doc_sha":"6176763fbac2fd22","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"($self, /)"},"numerator":{"doc_sha":"ed255748ce2eda7c","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"real":{"doc_sha":"d44497f1990af6fe","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"to_bytes":{"doc_sha":"b452319e48e64764","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"($self, /, length=1, byteorder='big', *, signed=False)"}}},"Vector":{"doc_sha":"0c8ae9e2ef387065","kind":"class","lifecycle":{"since":"0.2.8"},"members":{"angle":{"doc_sha":"a0ca5f1f4a89d780","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"angle() -> float"},"copy":{"doc_sha":"120e723fd513e072","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"copy() -> Vector"},"distance_to":{"doc_sha":"4f5d26bbe8434f65","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"distance_to(other: Vector) -> float"},"dot":{"doc_sha":"d69442ea289adfb8","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"dot(other: Vector) -> float"},"floor":{"doc_sha":"c7e81a90c5420968","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"floor() -> Vector"},"int":{"doc_sha":"3d97cae0ad8a963c","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"magnitude":{"doc_sha":"75f61a474afe3121","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"magnitude() -> float"},"magnitude_squared":{"doc_sha":"c976db5b540f827b","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"magnitude_squared() -> float"},"normalize":{"doc_sha":"04b468da3fc60858","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"normalize() -> Vector"},"x":{"doc_sha":"0e322bdd857e652b","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"y":{"doc_sha":"bbac84f15b6740d9","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""}}},"Viewport3D":{"doc_sha":"c41e58effff86261","kind":"class","lifecycle":{"since":"0.2.8"},"members":{"add_billboard":{"doc_sha":"ae2357f78a17a437","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"add_billboard(billboard)"},"add_layer":{"doc_sha":"b4c0274f9217aaa1","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"add_layer(name, z_index=0) -> dict"},"add_mesh":{"doc_sha":"588dbdf9a48f17e5","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"add_mesh(layer_name, model, pos, rotation=0, scale=1.0) -> int"},"add_voxel_layer":{"doc_sha":"47f17324425647e1","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"add_voxel_layer(voxel_grid, z_index=0)"},"animate":{"doc_sha":"400df4f5893261c5","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"animate(property: str, target: Any, duration: float, easing=None, delta=False, loop=False, callback=None, conflict_mode='replace') -> Animation"},"apply_heightmap":{"doc_sha":"be8e92a25726238d","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"apply_heightmap(heightmap, y_scale=1.0)"},"apply_terrain_colors":{"doc_sha":"cf0e47c185484e70","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"apply_terrain_colors(layer_name, r_map, g_map, b_map)"},"apply_threshold":{"doc_sha":"3adda69e435613d7","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"apply_threshold(heightmap, min_height, max_height, walkable=True)"},"at":{"doc_sha":"7ec76d7602627cb9","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"at(x, z) -> VoxelPoint"},"bg_color":{"doc_sha":"c7e7141bd1cadaea","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"billboard_count":{"doc_sha":"4b432c5c560ee44f","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"billboard_count() -> int"},"bounds":{"doc_sha":"d3186ac3b3ff8bed","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"build_terrain":{"doc_sha":"5be2a61f78907326","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"build_terrain(layer_name, heightmap, y_scale=1.0, cell_size=1.0) -> int"},"camera_pos":{"doc_sha":"7bfead8afc2b8312","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"camera_target":{"doc_sha":"3ecf7bb4dbdf27e4","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"cell_size":{"doc_sha":"8c3fa657c8fdc2aa","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"clear_billboards":{"doc_sha":"63d6d64c39b46264","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"clear_billboards()"},"clear_meshes":{"doc_sha":"1d6eb71f274020cf","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"clear_meshes(layer_name)"},"clear_voxel_nav_region":{"doc_sha":"56901862e4dc595a","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"clear_voxel_nav_region(voxel_grid)"},"compute_fov":{"doc_sha":"93c68ed57619564e","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"compute_fov(origin, radius=10) -> list"},"enable_affine":{"doc_sha":"96ba76ed43593940","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"enable_dither":{"doc_sha":"da6733dd08751b72","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"enable_fog":{"doc_sha":"ca6c5ffcef454868","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"enable_vertex_snap":{"doc_sha":"7401ad5dcd82024c","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"entities":{"doc_sha":"c2b185a3b718cc6b","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"find_path":{"doc_sha":"ebd174d40557cc19","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"find_path(start, end) -> list"},"fog_color":{"doc_sha":"3eb0cdbd75c5a94c","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"fog_far":{"doc_sha":"90406f8cae40bb58","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"fog_near":{"doc_sha":"e49f6dc79b22a794","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"follow":{"doc_sha":"975d2b945069be26","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"follow(entity, distance=10, height=5, smoothing=1.0)"},"fov":{"doc_sha":"7f5d69a1c423f69d","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"get_billboard":{"doc_sha":"38b0410480ab7d4d","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"get_billboard(index) -> Billboard"},"get_layer":{"doc_sha":"92223f274b5312c3","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"get_layer(name) -> dict or None"},"global_bounds":{"doc_sha":"aa4de9a20df55c2b","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"global_position":{"doc_sha":"b8dabbecc9fccd2a","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"grid_size":{"doc_sha":"7b4dbfa47f259f02","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"h":{"doc_sha":"d7308cd01fffb4be","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"hovered":{"doc_sha":"af0d353ce08f7be0","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"is_in_fov":{"doc_sha":"81d50fbbe2df4c8e","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"is_in_fov(x, z) -> bool"},"layer_count":{"doc_sha":"8bfbb28860b18d51","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"layer_count() -> int"},"move":{"doc_sha":"5c74f429970a2d2b","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"move(dx, dy) or (delta) -> None"},"on_click":{"doc_sha":"b3b8de4e294d71b6","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"on_enter":{"doc_sha":"48c996802e262396","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"on_exit":{"doc_sha":"c14ef0d6d6d40b0e","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"on_move":{"doc_sha":"9c3e26818b5b25b0","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"opacity":{"doc_sha":"c1a7ef41e074519e","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"orbit_camera":{"doc_sha":"29e02de46a996495","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"orbit_camera(angle=0, distance=10, height=5)"},"parent":{"doc_sha":"171b0de4e295c55b","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"place_blocking":{"doc_sha":"9252fd1d594a3867","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"place_blocking(grid_pos, footprint, walkable=False, transparent=False)"},"pos":{"doc_sha":"c1693d7dd0fe875d","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"project_all_voxels_to_nav":{"doc_sha":"ac9e0ca27b557d65","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"project_all_voxels_to_nav(headroom=2)"},"project_voxel_to_nav":{"doc_sha":"1fdf58bc686f7511","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"project_voxel_to_nav(voxel_grid, headroom=2)"},"realign":{"doc_sha":"8dfa7333a49aa697","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"realign() -> None"},"remove_billboard":{"doc_sha":"bf68668896f86bb0","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"remove_billboard(billboard)"},"remove_layer":{"doc_sha":"f5fafdfe3d153aab","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"remove_layer(name) -> bool"},"remove_voxel_layer":{"doc_sha":"b88a956cacc361cb","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"remove_voxel_layer(voxel_grid) -> bool"},"render_resolution":{"doc_sha":"4b141c5648005f04","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"resize":{"doc_sha":"005a04f1d129ebe5","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"resize(width, height) or (size) -> None"},"screen_to_world":{"doc_sha":"ba31510737afd516","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"screen_to_world(x, y, y_plane=0.0) -> tuple or None"},"set_grid_size":{"doc_sha":"ee7d1626547614c8","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"set_grid_size(width, depth)"},"set_slope_cost":{"doc_sha":"dac6d3cd319f1016","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"set_slope_cost(max_slope=0.5, cost_multiplier=1.0)"},"visible":{"doc_sha":"49221a4ae830f3b8","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"voxel_layer_count":{"doc_sha":"c1ca5c90881bbaae","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"voxel_layer_count() -> int"},"w":{"doc_sha":"4c5bcc34301022a0","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"x":{"doc_sha":"cd81d9dc457285aa","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"y":{"doc_sha":"406570900a1cf520","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"z_index":{"doc_sha":"328d87ae44d9fb71","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""}}},"VoxelGrid":{"doc_sha":"6706e3f5276ce104","kind":"class","lifecycle":{"since":"0.2.8"},"members":{"add_material":{"doc_sha":"27dec1c992cf7b99","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"add_material(name, color=Color(255,255,255), sprite_index=-1, transparent=False, path_cost=1.0) -> int"},"cell_size":{"doc_sha":"81b167d608b2cec5","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"clear":{"doc_sha":"8ebe20948a33a84d","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"clear() -> None"},"copy_region":{"doc_sha":"e56c5d28d7fd4e86","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"copy_region(min_coord, max_coord) -> VoxelRegion"},"count_material":{"doc_sha":"c9ef38e1f2f29ab8","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"count_material(material) -> int"},"count_non_air":{"doc_sha":"275d512b30f5134a","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"count_non_air() -> int"},"depth":{"doc_sha":"21671acfc312aeeb","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"fill":{"doc_sha":"fff2fcdddba5ee8c","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"fill(material) -> None"},"fill_box":{"doc_sha":"7c3d982a275f095d","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"fill_box(min_coord, max_coord, material) -> None"},"fill_box_hollow":{"doc_sha":"432e8b518feee0a5","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"fill_box_hollow(min_coord, max_coord, material, thickness=1) -> None"},"fill_cylinder":{"doc_sha":"0b2b0b0826381df0","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"fill_cylinder(base_pos, radius, height, material) -> None"},"fill_noise":{"doc_sha":"567355400611d920","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"fill_noise(min_coord, max_coord, material, threshold=0.5, scale=0.1, seed=0) -> None"},"fill_sphere":{"doc_sha":"e6a85d9c72e2ea44","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"fill_sphere(center, radius, material) -> None"},"from_bytes":{"doc_sha":"faefede0aa8ae636","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"from_bytes(data) -> bool"},"get":{"doc_sha":"52cea4645f0ea3d0","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"get(x, y, z) -> int"},"get_material":{"doc_sha":"2a61f6d54086df97","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"get_material(id) -> dict"},"greedy_meshing":{"doc_sha":"17fffa950c043917","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"height":{"doc_sha":"f271e989afe69da3","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"load":{"doc_sha":"5dcaf71057f836a2","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"load(path) -> bool"},"material_count":{"doc_sha":"0ef72bd68ef0c2d0","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"offset":{"doc_sha":"75230579ca06d8f5","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"paste_region":{"doc_sha":"1f44bacefd614cca","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"paste_region(region, position, skip_air=True) -> None"},"project_column":{"doc_sha":"9bacd59e3a7c5758","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"project_column(x, z, headroom=2) -> dict"},"rebuild_mesh":{"doc_sha":"fa60051037c3328c","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"rebuild_mesh() -> None"},"rotation":{"doc_sha":"5b603e8c652baf50","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"save":{"doc_sha":"bb7090694292f8e3","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"save(path) -> bool"},"set":{"doc_sha":"e9223501162aa9c6","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"set(x, y, z, material) -> None"},"size":{"doc_sha":"2d283d0584913b16","kind":"property","lifecycle":{"since":"0.2.8"},"signature":"Dimensions (width, height, depth) of the grid. Read-only."},"to_bytes":{"doc_sha":"f9a751e75bfc0478","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"to_bytes() -> bytes"},"vertex_count":{"doc_sha":"4df863704c32ecb7","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"visible":{"doc_sha":"7ff1afdbd4bf86a3","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"width":{"doc_sha":"0977aff160584918","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""}}},"VoxelRegion":{"doc_sha":"0cda0630ba7cafef","kind":"class","lifecycle":{"since":"0.2.8"},"members":{"depth":{"doc_sha":"05b3223f7df8ab65","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"height":{"doc_sha":"04834a15ca591472","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"size":{"doc_sha":"75c252385afbc498","kind":"property","lifecycle":{"since":"0.2.8"},"signature":"Dimensions (width, height, depth) of the region. Read-only."},"width":{"doc_sha":"d10d49e58037417d","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""}}},"WangSet":{"doc_sha":"320e161ef3d08a5d","kind":"class","lifecycle":{"since":"0.2.8"},"members":{"apply":{"doc_sha":"2aa8c0ba2d6549d2","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"apply(discrete_map: DiscreteMap, tile_layer: TileLayer) -> None"},"color_count":{"doc_sha":"13b244a538aec70f","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"colors":{"doc_sha":"6d84b3c2dd048b1a","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"name":{"doc_sha":"8b4560b705e5724f","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"resolve":{"doc_sha":"fe2fa09430c4bda6","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"resolve(discrete_map: DiscreteMap) -> list[int]"},"terrain_enum":{"doc_sha":"b8c46ec1a2b03eb6","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"terrain_enum() -> IntEnum"},"type":{"doc_sha":"ea3706e2c65ba7d3","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""}}},"Window":{"doc_sha":"6021e9b58544c378","kind":"class","lifecycle":{"since":"0.2.8"},"members":{"center":{"doc_sha":"91fe1b0d6ca2e4f1","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"center() -> None"},"framerate_limit":{"doc_sha":"de182ed271fbb4d3","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"fullscreen":{"doc_sha":"a517f5331026f2c4","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"game_resolution":{"doc_sha":"3a0dba59fecf5a95","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"get":{"doc_sha":"544bdc10ab93d067","kind":"classmethod","lifecycle":{"since":"0.2.8"},"signature":"get() -> Window"},"resolution":{"doc_sha":"548abee10c4fbcaf","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"scaling_mode":{"doc_sha":"57e03f9ffc9291ae","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"screenshot":{"doc_sha":"5102ab9c4c2c8358","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"screenshot(filename: str = None) -> bytes | None"},"title":{"doc_sha":"0d98279e5ec1b8b1","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"visible":{"doc_sha":"bad7f81ec2e48a31","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"vsync":{"doc_sha":"0335eb1fba386e37","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""}}}},"schema":1,"seeded":false,"version":"0.2.8-dev"} +{"commit":"43a6cc3","dirty":true,"functions":{"bresenham":{"doc_sha":"ee2d0f7a54a664f5","kind":"function","lifecycle":{"since":"0.2.8"},"signature":"bresenham(start, end, include_start=True, include_end=True) -> list[tuple[int, int]]"},"end_benchmark":{"doc_sha":"5d783c5dabb7a4e9","kind":"function","lifecycle":{"since":"0.2.8"},"signature":"end_benchmark() -> str"},"exit":{"doc_sha":"7572d4d6b1a1c8a4","kind":"function","lifecycle":{"since":"0.2.8"},"signature":"exit() -> None"},"find":{"doc_sha":"364b95807edcf8b4","kind":"function","lifecycle":{"since":"0.2.8"},"signature":"find(name: str, scene: str = None) -> UIDrawable | None"},"find_all":{"doc_sha":"b4a278f16b172131","kind":"function","lifecycle":{"since":"0.2.8"},"signature":"find_all(pattern: str, scene: str = None) -> list"},"get_metrics":{"doc_sha":"a0bbee3858baa4a9","kind":"function","lifecycle":{"modified":{"commit":"464af6a","version":"0.2.8-dev"},"since":"0.2.8"},"signature":"get_metrics() -> dict"},"lock":{"doc_sha":"e42989cabc81d080","kind":"function","lifecycle":{"since":"0.2.8"},"signature":"lock() -> _LockContext"},"log_benchmark":{"doc_sha":"4564bf7321366a0a","kind":"function","lifecycle":{"since":"0.2.8"},"signature":"log_benchmark(message: str) -> None"},"set_dev_console":{"doc_sha":"940ed19ff4f5243f","kind":"function","lifecycle":{"since":"0.2.8"},"signature":"set_dev_console(enabled: bool) -> None"},"set_scale":{"doc_sha":"b565b441a326af48","kind":"function","lifecycle":{"since":"0.2.8"},"signature":"set_scale(multiplier: float) -> None"},"start_benchmark":{"doc_sha":"783957f3517518fe","kind":"function","lifecycle":{"since":"0.2.8"},"signature":"start_benchmark() -> None"},"step":{"doc_sha":"94e778b7e791028a","kind":"function","lifecycle":{"modified":{"commit":"a7ba486","version":"0.2.8-dev"},"since":"0.2.8"},"signature":"step(dt: float = None) -> float"}},"module_attributes":{"animations":{"doc_sha":"30d25725feac9eb6","kind":"attribute","lifecycle":{"since":"0.2.8-dev"},"readonly":true},"current_scene":{"doc_sha":"ab496ea99fec2fa8","kind":"attribute","lifecycle":{"since":"0.2.8-dev"},"readonly":false},"default_transition":{"doc_sha":"73fb568e9cb25540","kind":"attribute","lifecycle":{"since":"0.2.8-dev"},"readonly":false},"default_transition_duration":{"doc_sha":"ae57530eded49138","kind":"attribute","lifecycle":{"since":"0.2.8-dev"},"readonly":false},"save_dir":{"doc_sha":"96ffa5a27d178b14","kind":"attribute","lifecycle":{"since":"0.2.8-dev"},"readonly":true},"scenes":{"doc_sha":"8021c54383d3cb8e","kind":"attribute","lifecycle":{"since":"0.2.8-dev"},"readonly":true},"timers":{"doc_sha":"48e2aa7747d8f527","kind":"attribute","lifecycle":{"since":"0.2.8-dev"},"readonly":true}},"objects":{"AStarPath":{"doc_sha":"60dd12bb4a54f3dd","kind":"class","lifecycle":{"since":"0.2.8"},"members":{"destination":{"doc_sha":"62a81c7267ad9ed9","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"origin":{"doc_sha":"c7dd130b8d4ef800","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"peek":{"doc_sha":"14054f10b4d9dda0","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"peek() -> Vector"},"remaining":{"doc_sha":"c193adc4bbfa3564","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"walk":{"doc_sha":"3fb64b239109d301","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"walk() -> Vector"}}},"Alignment":{"doc_sha":"77d93d03c8994f84","kind":"class","lifecycle":{"since":"0.2.8"},"members":{"as_integer_ratio":{"doc_sha":"3eb115fab0d0e0ec","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"($self, /)"},"bit_count":{"doc_sha":"f276da700b5a2e37","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"($self, /)"},"bit_length":{"doc_sha":"15b29db62fd0fa6c","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"($self, /)"},"conjugate":{"doc_sha":"6517a002faad7c75","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"($self, /)"},"denominator":{"doc_sha":"5051eb1570768851","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"from_bytes":{"doc_sha":"64672607865643a7","kind":"classmethod","lifecycle":{"since":"0.2.8"},"signature":"($type, /, bytes, byteorder='big', *, signed=False)"},"imag":{"doc_sha":"ef78ea410df39b26","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"is_integer":{"doc_sha":"6176763fbac2fd22","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"($self, /)"},"numerator":{"doc_sha":"ed255748ce2eda7c","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"real":{"doc_sha":"d44497f1990af6fe","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"to_bytes":{"doc_sha":"b452319e48e64764","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"($self, /, length=1, byteorder='big', *, signed=False)"}}},"Arc":{"doc_sha":"8323f4b7101509e9","kind":"class","lifecycle":{"since":"0.2.8"},"members":{"align":{"doc_sha":"58657c3cfa010f31","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"animate":{"doc_sha":"400df4f5893261c5","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"animate(property: str, target: Any, duration: float, easing=None, delta=False, loop=False, callback=None, conflict_mode='replace') -> Animation"},"bounds":{"doc_sha":"d3186ac3b3ff8bed","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"center":{"doc_sha":"abf1643dda106bf8","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"color":{"doc_sha":"bdf932405b56c319","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"end_angle":{"doc_sha":"74f9704909a3a23d","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"global_bounds":{"doc_sha":"aa4de9a20df55c2b","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"global_position":{"doc_sha":"b8dabbecc9fccd2a","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"grid_pos":{"doc_sha":"9df77e9a996208ba","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"grid_size":{"doc_sha":"e32208817299dfe9","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"horiz_margin":{"doc_sha":"0ef227a4cae6e675","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"hovered":{"doc_sha":"af0d353ce08f7be0","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"margin":{"doc_sha":"ab774a7da8cfe9bf","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"move":{"doc_sha":"5c74f429970a2d2b","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"move(dx, dy) or (delta) -> None"},"name":{"doc_sha":"0cc0684f26d8a660","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"on_click":{"doc_sha":"f2ef657aaf8b6c68","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"on_enter":{"doc_sha":"48c996802e262396","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"on_exit":{"doc_sha":"c14ef0d6d6d40b0e","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"on_move":{"doc_sha":"9c3e26818b5b25b0","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"opacity":{"doc_sha":"c1a7ef41e074519e","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"origin":{"doc_sha":"270bdf366c74bb1d","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"parent":{"doc_sha":"171b0de4e295c55b","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"pos":{"doc_sha":"6612fa35c43918e7","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"radius":{"doc_sha":"f9c2310b775f1e5a","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"realign":{"doc_sha":"8dfa7333a49aa697","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"realign() -> None"},"resize":{"doc_sha":"005a04f1d129ebe5","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"resize(width, height) or (size) -> None"},"rotate_with_camera":{"doc_sha":"5c3af880218d3a08","kind":"property","lifecycle":{"modified":{"commit":"4bb1966","version":"0.2.8-dev"},"since":"0.2.8"},"signature":""},"rotation":{"doc_sha":"61ba3e66a0841a08","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"start_angle":{"doc_sha":"6b91f175ebe5ecb4","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"thickness":{"doc_sha":"f8eba89682554e6d","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"vert_margin":{"doc_sha":"e2042be923bc6fcb","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"visible":{"doc_sha":"49221a4ae830f3b8","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"z_index":{"doc_sha":"67dd517aa153c158","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""}}},"AutoRuleSet":{"doc_sha":"49ab716632a44f06","kind":"class","lifecycle":{"since":"0.2.8"},"members":{"apply":{"doc_sha":"701b6f038cfd6809","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"apply(discrete_map: DiscreteMap, tile_layer: TileLayer, seed: int = 0) -> None"},"grid_size":{"doc_sha":"570f4c81acb781d8","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"group_count":{"doc_sha":"892fc283939b3e0c","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"name":{"doc_sha":"4f5ca90860649b64","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"resolve":{"doc_sha":"f8fff4e6e6488e3f","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"resolve(discrete_map: DiscreteMap, seed: int = 0) -> list[int]"},"rule_count":{"doc_sha":"9b9d5a7c1475dcfa","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"terrain_enum":{"doc_sha":"eb365bf99bbcca37","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"terrain_enum() -> IntEnum"},"value_count":{"doc_sha":"3467d158dd5af073","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"values":{"doc_sha":"b3199e059fd1ca2a","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""}}},"BSP":{"doc_sha":"7e971e0542cf4006","kind":"class","lifecycle":{"since":"0.2.8"},"members":{"adjacency":{"doc_sha":"9373aa85408486ba","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"bounds":{"doc_sha":"9c41e7403f2a0b2f","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"clear":{"doc_sha":"2e4ffd3360ac858e","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"clear() -> BSP"},"find":{"doc_sha":"fccfac484a2b889c","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"find(pos: tuple[int, int] | list | Vector) -> BSPNode | None"},"get_leaf":{"doc_sha":"d8ba680ec2ad4fd1","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"get_leaf(index: int) -> BSPNode"},"leaves":{"doc_sha":"439ec78ec02e6df8","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"leaves() -> Iterator[BSPNode]"},"pos":{"doc_sha":"b8b27f4ad76e2870","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"root":{"doc_sha":"d1e3d9741dab383d","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"size":{"doc_sha":"621f11f9a37dd89d","kind":"property","lifecycle":{"since":"0.2.8"},"signature":"Dimensions (width, height). Read-only."},"split_once":{"doc_sha":"0d7c0c33156bc4e5","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"split_once(horizontal: bool, position: int) -> BSP"},"split_recursive":{"doc_sha":"fa516b79833e5094","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"split_recursive(depth: int, min_size: tuple[int, int], max_ratio: float = 1.5, seed: int | None = None) -> BSP"},"to_heightmap":{"doc_sha":"a0f03cab7a0f2d1b","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"to_heightmap(size: tuple[int, int] | None = None, select: str = 'leaves', shrink: int = 0, value: float = 1.0) -> HeightMap"},"traverse":{"doc_sha":"57da83e0f37686aa","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"traverse(order: Traversal = Traversal.LEVEL_ORDER) -> Iterator[BSPNode]"}}},"Behavior":{"doc_sha":"09206365e6c37ecc","kind":"class","lifecycle":{"since":"0.2.8"},"members":{"as_integer_ratio":{"doc_sha":"3eb115fab0d0e0ec","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"($self, /)"},"bit_count":{"doc_sha":"f276da700b5a2e37","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"($self, /)"},"bit_length":{"doc_sha":"15b29db62fd0fa6c","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"($self, /)"},"conjugate":{"doc_sha":"6517a002faad7c75","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"($self, /)"},"denominator":{"doc_sha":"5051eb1570768851","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"from_bytes":{"doc_sha":"64672607865643a7","kind":"classmethod","lifecycle":{"since":"0.2.8"},"signature":"($type, /, bytes, byteorder='big', *, signed=False)"},"imag":{"doc_sha":"ef78ea410df39b26","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"is_integer":{"doc_sha":"6176763fbac2fd22","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"($self, /)"},"numerator":{"doc_sha":"ed255748ce2eda7c","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"real":{"doc_sha":"d44497f1990af6fe","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"to_bytes":{"doc_sha":"b452319e48e64764","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"($self, /, length=1, byteorder='big', *, signed=False)"}}},"Billboard":{"doc_sha":"38614d1d19942bab","kind":"class","lifecycle":{"since":"0.2.8"},"members":{"facing":{"doc_sha":"f82e2d7d8087804a","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"opacity":{"doc_sha":"dc5ed39915ab896c","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"phi":{"doc_sha":"1450c89fb5b47929","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"pos":{"doc_sha":"0d5fec7fd4701ea6","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"scale":{"doc_sha":"c9d55c4579681854","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"sprite_index":{"doc_sha":"170c20c841b586fc","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"texture":{"doc_sha":"b6d846566b6ccf93","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"theta":{"doc_sha":"c214ecbcb099228b","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"visible":{"doc_sha":"adf8070be9a09eba","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""}}},"CallableBinding":{"doc_sha":"711bf0f149ccae7a","kind":"class","lifecycle":{"since":"0.2.8"},"members":{"callable":{"doc_sha":"96d3d31a6b7c56d9","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"is_valid":{"doc_sha":"bc69c90e13475ae6","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"value":{"doc_sha":"8f7d9eb4f520c017","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""}}},"Caption":{"doc_sha":"8cd46324baa46a0d","kind":"class","lifecycle":{"since":"0.2.8"},"members":{"align":{"doc_sha":"58657c3cfa010f31","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"animate":{"doc_sha":"400df4f5893261c5","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"animate(property: str, target: Any, duration: float, easing=None, delta=False, loop=False, callback=None, conflict_mode='replace') -> Animation"},"bounds":{"doc_sha":"d3186ac3b3ff8bed","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"fill_color":{"doc_sha":"af7ba90df95e4b79","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"font":{"doc_sha":"fbdafdb33921aa13","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"font_size":{"doc_sha":"7894149ec4efec6e","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"global_bounds":{"doc_sha":"aa4de9a20df55c2b","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"global_position":{"doc_sha":"b8dabbecc9fccd2a","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"grid_pos":{"doc_sha":"79a7d48503e657d1","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"grid_size":{"doc_sha":"25b029c86a618fee","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"h":{"doc_sha":"ca1a98d700ff1888","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"horiz_margin":{"doc_sha":"0ef227a4cae6e675","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"hovered":{"doc_sha":"af0d353ce08f7be0","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"margin":{"doc_sha":"ab774a7da8cfe9bf","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"move":{"doc_sha":"5c74f429970a2d2b","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"move(dx, dy) or (delta) -> None"},"name":{"doc_sha":"90a2ab0ec07c00eb","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"on_click":{"doc_sha":"b3b8de4e294d71b6","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"on_enter":{"doc_sha":"48c996802e262396","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"on_exit":{"doc_sha":"c14ef0d6d6d40b0e","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"on_move":{"doc_sha":"9c3e26818b5b25b0","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"opacity":{"doc_sha":"c1a7ef41e074519e","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"origin":{"doc_sha":"270bdf366c74bb1d","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"outline":{"doc_sha":"6580b31092048bcb","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"outline_color":{"doc_sha":"388919803d270fd3","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"parent":{"doc_sha":"171b0de4e295c55b","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"pos":{"doc_sha":"44e4967e15212959","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"realign":{"doc_sha":"8dfa7333a49aa697","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"realign() -> None"},"resize":{"doc_sha":"005a04f1d129ebe5","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"resize(width, height) or (size) -> None"},"rotate_with_camera":{"doc_sha":"5c3af880218d3a08","kind":"property","lifecycle":{"modified":{"commit":"4bb1966","version":"0.2.8-dev"},"since":"0.2.8"},"signature":""},"rotation":{"doc_sha":"61ba3e66a0841a08","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"shader":{"doc_sha":"418952064319ffb5","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"size":{"doc_sha":"85abb564299b703d","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"text":{"doc_sha":"41628142b287defe","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"uniforms":{"doc_sha":"b1f2699da618d002","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"vert_margin":{"doc_sha":"e2042be923bc6fcb","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"visible":{"doc_sha":"49221a4ae830f3b8","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"w":{"doc_sha":"8472810bca891509","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"x":{"doc_sha":"528487f8deaaf789","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"y":{"doc_sha":"114ecec940477107","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"z_index":{"doc_sha":"328d87ae44d9fb71","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""}}},"Circle":{"doc_sha":"077981a1fbdfb6e3","kind":"class","lifecycle":{"since":"0.2.8"},"members":{"align":{"doc_sha":"58657c3cfa010f31","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"animate":{"doc_sha":"400df4f5893261c5","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"animate(property: str, target: Any, duration: float, easing=None, delta=False, loop=False, callback=None, conflict_mode='replace') -> Animation"},"bounds":{"doc_sha":"d3186ac3b3ff8bed","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"center":{"doc_sha":"d1e80ccd6bf706b9","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"fill_color":{"doc_sha":"46be64336e296c50","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"global_bounds":{"doc_sha":"aa4de9a20df55c2b","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"global_position":{"doc_sha":"b8dabbecc9fccd2a","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"grid_pos":{"doc_sha":"20bebfa7432ed339","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"grid_size":{"doc_sha":"b43e33c2d49a3565","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"horiz_margin":{"doc_sha":"0ef227a4cae6e675","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"hovered":{"doc_sha":"af0d353ce08f7be0","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"margin":{"doc_sha":"ab774a7da8cfe9bf","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"move":{"doc_sha":"5c74f429970a2d2b","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"move(dx, dy) or (delta) -> None"},"name":{"doc_sha":"0cc0684f26d8a660","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"on_click":{"doc_sha":"3a340282000297e0","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"on_enter":{"doc_sha":"48c996802e262396","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"on_exit":{"doc_sha":"c14ef0d6d6d40b0e","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"on_move":{"doc_sha":"9c3e26818b5b25b0","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"opacity":{"doc_sha":"c1a7ef41e074519e","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"origin":{"doc_sha":"270bdf366c74bb1d","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"outline":{"doc_sha":"20e9fd42a7ba4bb8","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"outline_color":{"doc_sha":"3fbed883fee308fe","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"parent":{"doc_sha":"171b0de4e295c55b","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"pos":{"doc_sha":"5f9d88ff075d2cfc","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"radius":{"doc_sha":"00a41207192c55a8","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"realign":{"doc_sha":"8dfa7333a49aa697","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"realign() -> None"},"resize":{"doc_sha":"005a04f1d129ebe5","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"resize(width, height) or (size) -> None"},"rotate_with_camera":{"doc_sha":"5c3af880218d3a08","kind":"property","lifecycle":{"modified":{"commit":"4bb1966","version":"0.2.8-dev"},"since":"0.2.8"},"signature":""},"rotation":{"doc_sha":"61ba3e66a0841a08","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"vert_margin":{"doc_sha":"e2042be923bc6fcb","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"visible":{"doc_sha":"49221a4ae830f3b8","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"z_index":{"doc_sha":"0cb3df5278d2658b","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""}}},"Color":{"doc_sha":"f1f664405900332f","kind":"class","lifecycle":{"since":"0.2.8"},"members":{"a":{"doc_sha":"ef61630ad34f8e2a","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"b":{"doc_sha":"2e2e91e93833fdd0","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"from_hex":{"doc_sha":"24d1ff553133bb7e","kind":"classmethod","lifecycle":{"since":"0.2.8"},"signature":"from_hex(hex_string: str) -> Color"},"g":{"doc_sha":"716ced374bcc2dc4","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"lerp":{"doc_sha":"290e204f3538e406","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"lerp(other: Color, t: float) -> Color"},"r":{"doc_sha":"ef64cae6296f68cb","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"to_hex":{"doc_sha":"d0cf65b284158877","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"to_hex() -> str"}}},"ColorLayer":{"doc_sha":"f8a87b6184c9a9b6","kind":"class","lifecycle":{"since":"0.2.8"},"members":{"apply_gradient":{"doc_sha":"20390111f0652ae0","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"apply_gradient(source: HeightMap, range: tuple, color_low: Color, color_high: Color) -> ColorLayer"},"apply_perspective":{"doc_sha":"a909e1b1c0373bc0","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"apply_perspective(entity: Entity, visible: Color | None = None, discovered: Color | None = None, unknown: Color | None = None) -> None"},"apply_ranges":{"doc_sha":"37fc5a26fda9cc0c","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"apply_ranges(source: HeightMap, ranges: list) -> ColorLayer"},"apply_threshold":{"doc_sha":"59cc9b72322cc677","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"apply_threshold(source: HeightMap, range: tuple, color: Color) -> ColorLayer"},"at":{"doc_sha":"fef47313dcef9247","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"at(pos: tuple | Vector) or (x: int, y: int) -> Color"},"clear_perspective":{"doc_sha":"fec237fe4cbda340","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"clear_perspective() -> None"},"draw_fov":{"doc_sha":"ff965b12c888d092","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"draw_fov(source: tuple, radius: int | None = None, fov: FOV | None = None, visible: Color | None = None, discovered: Color | None = None, unknown: Color | None = None) -> None"},"edit":{"doc_sha":"b0edb9c8f4c1fe76","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"edit() -> context manager"},"fill":{"doc_sha":"f6ff376fd71ae90b","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"fill(color: Color) -> None"},"fill_rect":{"doc_sha":"bc8b20da214aa37d","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"fill_rect(pos: tuple, size: tuple, color: Color) -> None"},"grid":{"doc_sha":"b965c55d57de78fa","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"grid_size":{"doc_sha":"eac88eb0f7b16cbf","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"name":{"doc_sha":"dd282cc9273e2e36","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"set":{"doc_sha":"36801cc04606ee0b","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"set(pos, color: Color) -> None"},"update_perspective":{"doc_sha":"c9caac507fbc4921","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"update_perspective() -> None"},"visible":{"doc_sha":"ecea81ec16e31c19","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"z_index":{"doc_sha":"eb13c1b4de11ce63","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""}}},"DijkstraMap":{"doc_sha":"822ccf5b086d37ec","kind":"class","lifecycle":{"since":"0.2.8"},"members":{"descent_step":{"doc_sha":"fd7b649f1f3eb586","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"descent_step(pos: Vector | tuple) -> Vector | None"},"distance":{"doc_sha":"92b40108bb47bf88","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"distance(pos: Vector | tuple) -> float | None"},"invert":{"doc_sha":"3e5ae145dafc2c1d","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"invert() -> DijkstraMap"},"path_from":{"doc_sha":"3dcf638bff837f2d","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"path_from(pos: Vector | tuple) -> AStarPath"},"root":{"doc_sha":"95824bac9c46ad83","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"step_from":{"doc_sha":"ac9bc93f671d3936","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"step_from(pos: Vector | tuple) -> Vector | None"},"to_heightmap":{"doc_sha":"0ba542e6509b510b","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"to_heightmap(size=None, unreachable=-1.0) -> HeightMap"}}},"DiscreteMap":{"doc_sha":"4d6d00ffa3ac62a9","kind":"class","lifecycle":{"since":"0.2.8"},"members":{"add":{"doc_sha":"1e5a5b83f555fa87","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"add(other: DiscreteMap | int, *, pos=None, source_pos=None, size=None) -> DiscreteMap"},"bitwise_and":{"doc_sha":"1644af83b8699792","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"bitwise_and(other: DiscreteMap, *, pos=None, source_pos=None, size=None) -> DiscreteMap"},"bitwise_or":{"doc_sha":"c8262af2fdccdeeb","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"bitwise_or(other: DiscreteMap, *, pos=None, source_pos=None, size=None) -> DiscreteMap"},"bitwise_xor":{"doc_sha":"01809471595bf8e7","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"bitwise_xor(other: DiscreteMap, *, pos=None, source_pos=None, size=None) -> DiscreteMap"},"bool":{"doc_sha":"a999f0f09edf7bf3","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"bool(condition: int | set | callable) -> DiscreteMap"},"clear":{"doc_sha":"b04d176e9c7bf9d2","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"clear() -> DiscreteMap"},"copy_from":{"doc_sha":"efea5a0aa9a8d9c9","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"copy_from(other: DiscreteMap, *, pos=None, source_pos=None, size=None) -> DiscreteMap"},"count":{"doc_sha":"5e3303ddc3a9e3ae","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"count(value: int) -> int"},"count_range":{"doc_sha":"ac4e0298919fd16e","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"count_range(min_val: int, max_val: int) -> int"},"enum_type":{"doc_sha":"f27f1ad46e2d5afe","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"fill":{"doc_sha":"7f358bdcc5c2c9f0","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"fill(value: int, *, pos=None, size=None) -> DiscreteMap"},"from_bytes":{"doc_sha":"b1c09c737d43ffa9","kind":"classmethod","lifecycle":{"since":"0.2.8"},"signature":"from_bytes(data: bytes, size: tuple[int, int], *, enum: type = None) -> DiscreteMap"},"from_heightmap":{"doc_sha":"0e89bb4262b19ea3","kind":"classmethod","lifecycle":{"since":"0.2.8"},"signature":"from_heightmap(hmap: HeightMap, mapping: list[tuple[tuple[float,float], int]], *, enum=None) -> DiscreteMap"},"get":{"doc_sha":"a1097b27b43d6a27","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"get(x, y) or (pos) -> int | Enum"},"histogram":{"doc_sha":"b2d9df4f14aedd51","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"histogram() -> dict[int, int]"},"invert":{"doc_sha":"0f52aecfe57ff499","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"invert() -> DiscreteMap"},"mask":{"doc_sha":"0d9628616f4592f4","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"mask() -> memoryview"},"max":{"doc_sha":"144265dd0da983a8","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"max(other: DiscreteMap, *, pos=None, source_pos=None, size=None) -> DiscreteMap"},"min":{"doc_sha":"ff90ceef60a45959","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"min(other: DiscreteMap, *, pos=None, source_pos=None, size=None) -> DiscreteMap"},"min_max":{"doc_sha":"22018caaf1bdb60e","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"min_max() -> tuple[int, int]"},"multiply":{"doc_sha":"430abcb2b647132f","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"multiply(factor: float, *, pos=None, size=None) -> DiscreteMap"},"set":{"doc_sha":"8439c14e5785f7e4","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"set(x: int, y: int, value: int) -> None"},"size":{"doc_sha":"fc6c37c9d3916c5d","kind":"property","lifecycle":{"since":"0.2.8"},"signature":"Dimensions (width, height) of the map. Read-only."},"subtract":{"doc_sha":"aeae8042663b9973","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"subtract(other: DiscreteMap | int, *, pos=None, source_pos=None, size=None) -> DiscreteMap"},"to_bytes":{"doc_sha":"4d7c4b5939a546bd","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"to_bytes() -> bytes"},"to_heightmap":{"doc_sha":"b0744db8f889b5d9","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"to_heightmap(mapping: dict[int, float] = None) -> HeightMap"}}},"Drawable":{"doc_sha":"bb28a8d677f24b33","kind":"class","lifecycle":{"since":"0.2.8"},"members":{"move":{"doc_sha":"4a8f9ca3692c6b58","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"move(dx, dy) or (delta) -> None"},"on_click":{"doc_sha":"b3b8de4e294d71b6","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"opacity":{"doc_sha":"c1a7ef41e074519e","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"resize":{"doc_sha":"980bdc9065fc077b","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"resize(width, height) or (size) -> None"},"visible":{"doc_sha":"49221a4ae830f3b8","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"z_index":{"doc_sha":"328d87ae44d9fb71","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""}}},"Easing":{"doc_sha":"e3b0c44298fc1c14","kind":"class","lifecycle":{"since":"0.2.8"},"members":{"as_integer_ratio":{"doc_sha":"3eb115fab0d0e0ec","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"($self, /)"},"bit_count":{"doc_sha":"f276da700b5a2e37","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"($self, /)"},"bit_length":{"doc_sha":"15b29db62fd0fa6c","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"($self, /)"},"conjugate":{"doc_sha":"6517a002faad7c75","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"($self, /)"},"denominator":{"doc_sha":"5051eb1570768851","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"from_bytes":{"doc_sha":"64672607865643a7","kind":"classmethod","lifecycle":{"since":"0.2.8"},"signature":"($type, /, bytes, byteorder='big', *, signed=False)"},"imag":{"doc_sha":"ef78ea410df39b26","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"is_integer":{"doc_sha":"6176763fbac2fd22","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"($self, /)"},"numerator":{"doc_sha":"ed255748ce2eda7c","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"real":{"doc_sha":"d44497f1990af6fe","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"to_bytes":{"doc_sha":"b452319e48e64764","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"($self, /, length=1, byteorder='big', *, signed=False)"}}},"Entity":{"doc_sha":"7231aaf2e3b107d0","kind":"class","lifecycle":{"since":"0.2.8"},"members":{"add_label":{"doc_sha":"be06d30bbd5a41ca","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"add_label(label: str) -> None"},"animate":{"doc_sha":"a06820cecef57a9a","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"animate(property: str, target: Any, duration: float, easing=None, delta=False, loop=False, callback=None, conflict_mode='replace') -> Animation"},"at":{"doc_sha":"b5775513388a1464","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"at(x: int, y: int) -> GridPoint | None"},"behavior_type":{"doc_sha":"745936b7029480fd","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"cell_pos":{"doc_sha":"f8feaba9f3652ca0","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"cell_x":{"doc_sha":"8bdc1199bfb15967","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"cell_y":{"doc_sha":"8fd78a55ede4b259","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"default_behavior":{"doc_sha":"f0a1ea99426408dd","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"die":{"doc_sha":"1369fecbc5a1862a","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"die() -> None"},"draw_pos":{"doc_sha":"2769bf67eec13406","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"find_path":{"doc_sha":"1a672b91a00eb597","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"find_path(target, diagonal_cost: float = 1.41, collide: str = None) -> AStarPath | None"},"grid":{"doc_sha":"d02c7b8c3beb5662","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"grid_pos":{"doc_sha":"d2e9f06a9d9f0a3c","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"grid_x":{"doc_sha":"4108b9ec31aff2c0","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"grid_y":{"doc_sha":"26f7190f34a208b2","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"has_label":{"doc_sha":"0e65dc586b3c8d18","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"has_label(label: str) -> bool"},"index":{"doc_sha":"74e24820bb380594","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"index() -> int"},"labels":{"doc_sha":"4e16ef3312d5b4e7","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"move":{"doc_sha":"5c74f429970a2d2b","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"move(dx, dy) or (delta) -> None"},"move_speed":{"doc_sha":"a3b3867a6d3b830a","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"name":{"doc_sha":"708f75f696069d69","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"opacity":{"doc_sha":"6caae59e941bebf1","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"path_to":{"doc_sha":"5b11e33c92caac2b","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"path_to(x: int, y: int) -> list"},"perspective_map":{"doc_sha":"f61894673d8c27a7","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"pos":{"doc_sha":"172ff9a95e621eaf","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"remove_label":{"doc_sha":"a4a541d7e9cf7275","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"remove_label(label: str) -> None"},"resize":{"doc_sha":"005a04f1d129ebe5","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"resize(width, height) or (size) -> None"},"set_behavior":{"doc_sha":"328826510c72f049","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"set_behavior(type, waypoints=None, turns: int = 0, path=None, pathfinder=None) -> None"},"shader":{"doc_sha":"04503c544358f827","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"sight_radius":{"doc_sha":"4a9ec6c3788b093a","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"sprite_grid":{"doc_sha":"a5ff2c3e5190bc41","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"sprite_index":{"doc_sha":"ca285ed1c6f460df","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"sprite_offset":{"doc_sha":"a92dae4a2555fa7b","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"sprite_offset_x":{"doc_sha":"7ba6504a9bbbf681","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"sprite_offset_y":{"doc_sha":"24d4546e182bb2c8","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"step":{"doc_sha":"8877861183e50020","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"target_label":{"doc_sha":"85acb11a7e713a41","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"texture":{"doc_sha":"1588a0d4779fd069","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"tile_height":{"doc_sha":"7f2ef7cb6d425730","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"tile_size":{"doc_sha":"1a647ababfde2daa","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"tile_width":{"doc_sha":"5d72ddf874e9db54","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"turn_order":{"doc_sha":"eba665bc9c7e75ee","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"uniforms":{"doc_sha":"d302457c5aaa495e","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"update_visibility":{"doc_sha":"d096cd998ed99b49","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"update_visibility() -> None"},"visible":{"doc_sha":"2bca3a265b44b270","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"visible_entities":{"doc_sha":"2482fbbecbfac7ea","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"visible_entities(fov=None, radius: int | None = None) -> list[Entity]"},"x":{"doc_sha":"4ba6a677ea35e7ba","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"y":{"doc_sha":"52b85f7dc8a7cb27","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""}}},"Entity3D":{"doc_sha":"0daee46cf6343e9c","kind":"class","lifecycle":{"since":"0.2.8"},"members":{"anim_clip":{"doc_sha":"f69294fc08fc1d6c","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"anim_frame":{"doc_sha":"41bdd9b6f4407d16","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"anim_loop":{"doc_sha":"b267a372c1aef2cd","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"anim_paused":{"doc_sha":"e100f0be03f09b09","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"anim_speed":{"doc_sha":"33c581c9533b5169","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"anim_time":{"doc_sha":"6b811c83dd2a2184","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"animate":{"doc_sha":"fcf44af080ce16a7","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"animate(property, target, duration, easing=None, delta=False, callback=None, conflict_mode=None)"},"at":{"doc_sha":"69d6101c79794f90","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"at(x, z) -> dict"},"auto_animate":{"doc_sha":"867ca7a9c2b5a4ab","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"clear_path":{"doc_sha":"989bb0a431b58e17","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"clear_path()"},"color":{"doc_sha":"ea9ab21659b6e392","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"follow_path":{"doc_sha":"f514b7092f561550","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"follow_path(path)"},"grid_pos":{"doc_sha":"f20411521f420193","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"idle_clip":{"doc_sha":"855c97611053652c","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"is_moving":{"doc_sha":"b31c1090228993e4","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"model":{"doc_sha":"26ae36cf867d07e9","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"name":{"doc_sha":"acea3a6577d65fbc","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"on_anim_complete":{"doc_sha":"5cc47894576d983d","kind":"property","lifecycle":{"since":"0.2.8"},"signature":"Callback(entity, clip_name) when non-looping animation ends."},"path_to":{"doc_sha":"d6fe3936cc8e2d84","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"path_to(x, z) or path_to(pos=(x, z)) -> list"},"pos":{"doc_sha":"2488492030e9df40","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"rotation":{"doc_sha":"5b603e8c652baf50","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"scale":{"doc_sha":"b06782214d92e3f7","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"teleport":{"doc_sha":"d53364830cdc737a","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"teleport(x, z) or teleport(pos=(x, z))"},"update_visibility":{"doc_sha":"61886fb70daedf6b","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"update_visibility()"},"viewport":{"doc_sha":"e9f9154f00b47f9a","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"visible":{"doc_sha":"bc1ae0f4c6b5cb11","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"walk_clip":{"doc_sha":"c95d11b4e1901f28","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"world_pos":{"doc_sha":"bcc6673be9243dd5","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""}}},"EntityCollection3D":{"doc_sha":"55864ac691527ca9","kind":"class","lifecycle":{"since":"0.2.8"},"members":{"append":{"doc_sha":"43922f93aff7d723","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"append(entity)"},"clear":{"doc_sha":"f44ea077b10186d7","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"clear()"},"extend":{"doc_sha":"4fd2e7e83437c7ae","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"extend(iterable)"},"find":{"doc_sha":"e4182a3a3317aaae","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"find(name) -> Entity3D or None"},"pop":{"doc_sha":"cbcc90e2a433f277","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"pop(index=-1) -> Entity3D"},"remove":{"doc_sha":"b2f8fb276b91ec1e","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"remove(entity)"}}},"EntityCollection3DIter":{"doc_sha":"d71ec48b224bfacc","kind":"class","lifecycle":{"since":"0.2.8"},"members":{}},"FOV":{"doc_sha":"e3b0c44298fc1c14","kind":"class","lifecycle":{"since":"0.2.8"},"members":{"as_integer_ratio":{"doc_sha":"3eb115fab0d0e0ec","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"($self, /)"},"bit_count":{"doc_sha":"f276da700b5a2e37","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"($self, /)"},"bit_length":{"doc_sha":"15b29db62fd0fa6c","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"($self, /)"},"conjugate":{"doc_sha":"6517a002faad7c75","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"($self, /)"},"denominator":{"doc_sha":"5051eb1570768851","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"from_bytes":{"doc_sha":"64672607865643a7","kind":"classmethod","lifecycle":{"since":"0.2.8"},"signature":"($type, /, bytes, byteorder='big', *, signed=False)"},"imag":{"doc_sha":"ef78ea410df39b26","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"is_integer":{"doc_sha":"6176763fbac2fd22","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"($self, /)"},"numerator":{"doc_sha":"ed255748ce2eda7c","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"real":{"doc_sha":"d44497f1990af6fe","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"to_bytes":{"doc_sha":"b452319e48e64764","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"($self, /, length=1, byteorder='big', *, signed=False)"}}},"Font":{"doc_sha":"245b4e9b0ca66fc9","kind":"class","lifecycle":{"since":"0.2.8"},"members":{"family":{"doc_sha":"0cf26d3038432764","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"source":{"doc_sha":"efc970576a584cde","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""}}},"Frame":{"doc_sha":"4d53c3cb9e9e4611","kind":"class","lifecycle":{"since":"0.2.8"},"members":{"align":{"doc_sha":"58657c3cfa010f31","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"animate":{"doc_sha":"400df4f5893261c5","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"animate(property: str, target: Any, duration: float, easing=None, delta=False, loop=False, callback=None, conflict_mode='replace') -> Animation"},"bounds":{"doc_sha":"d3186ac3b3ff8bed","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"cache_subtree":{"doc_sha":"0b8d1175192c1ce2","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"children":{"doc_sha":"b2cf42011b86727c","kind":"property","lifecycle":{"modified":{"commit":"4c0953c","version":"0.2.8-dev"},"since":"0.2.8"},"signature":""},"clip_children":{"doc_sha":"d12b2c4d1d2bb6cb","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"fill_color":{"doc_sha":"8cfbe5aa6dd6da27","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"global_bounds":{"doc_sha":"aa4de9a20df55c2b","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"global_position":{"doc_sha":"b8dabbecc9fccd2a","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"grid_pos":{"doc_sha":"2f5b05882c4ad39f","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"grid_size":{"doc_sha":"9c8707ad589e5d8a","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"h":{"doc_sha":"ced8010da828cc93","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"horiz_margin":{"doc_sha":"0ef227a4cae6e675","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"hovered":{"doc_sha":"af0d353ce08f7be0","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"margin":{"doc_sha":"ab774a7da8cfe9bf","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"move":{"doc_sha":"5c74f429970a2d2b","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"move(dx, dy) or (delta) -> None"},"name":{"doc_sha":"90a2ab0ec07c00eb","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"on_click":{"doc_sha":"b3b8de4e294d71b6","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"on_enter":{"doc_sha":"48c996802e262396","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"on_exit":{"doc_sha":"c14ef0d6d6d40b0e","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"on_move":{"doc_sha":"9c3e26818b5b25b0","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"opacity":{"doc_sha":"c1a7ef41e074519e","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"origin":{"doc_sha":"270bdf366c74bb1d","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"outline":{"doc_sha":"c07b6dec9da5b9ea","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"outline_color":{"doc_sha":"cd98fcaf39f0bc56","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"parent":{"doc_sha":"171b0de4e295c55b","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"pos":{"doc_sha":"55a118e7889fd555","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"realign":{"doc_sha":"8dfa7333a49aa697","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"realign() -> None"},"resize":{"doc_sha":"005a04f1d129ebe5","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"resize(width, height) or (size) -> None"},"rotate_with_camera":{"doc_sha":"5c3af880218d3a08","kind":"property","lifecycle":{"modified":{"commit":"4bb1966","version":"0.2.8-dev"},"since":"0.2.8"},"signature":""},"rotation":{"doc_sha":"61ba3e66a0841a08","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"shader":{"doc_sha":"418952064319ffb5","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"uniforms":{"doc_sha":"b1f2699da618d002","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"vert_margin":{"doc_sha":"e2042be923bc6fcb","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"visible":{"doc_sha":"49221a4ae830f3b8","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"w":{"doc_sha":"bd51299002a099c5","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"x":{"doc_sha":"4ed17c625f2289e3","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"y":{"doc_sha":"fabaa45fd789c7fd","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"z_index":{"doc_sha":"328d87ae44d9fb71","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""}}},"Grid":{"doc_sha":"f88e43e992d43a86","kind":"class","lifecycle":{"since":"0.2.8"},"members":{"align":{"doc_sha":"58657c3cfa010f31","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"animate":{"doc_sha":"400df4f5893261c5","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"animate(property: str, target: Any, duration: float, easing=None, delta=False, loop=False, callback=None, conflict_mode='replace') -> Animation"},"bounds":{"doc_sha":"d3186ac3b3ff8bed","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"camera_rotation":{"doc_sha":"bc2cd502f52817e9","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"center":{"doc_sha":"913ef5c36eaf9635","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"center_camera":{"doc_sha":"00d37ccdedcd3a2e","kind":"method","lifecycle":{"since":"0.2.8-dev"},"signature":"center_camera(pos: tuple = None) -> None"},"center_x":{"doc_sha":"fbb0ff08116f3c80","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"center_y":{"doc_sha":"1bda27633025fb12","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"children":{"doc_sha":"9566d92d5619ee21","kind":"property","lifecycle":{"since":"0.2.8-dev"},"signature":""},"fill_color":{"doc_sha":"f26fb5da3de1789a","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"global_bounds":{"doc_sha":"aa4de9a20df55c2b","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"global_position":{"doc_sha":"b8dabbecc9fccd2a","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"grid_data":{"doc_sha":"fdf0084b8456fd45","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"h":{"doc_sha":"bd5a06618a50efad","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"horiz_margin":{"doc_sha":"0ef227a4cae6e675","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"hovered":{"doc_sha":"af0d353ce08f7be0","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"hovered_cell":{"doc_sha":"85c87c8aa013d0c4","kind":"property","lifecycle":{"since":"0.2.8-dev"},"signature":""},"margin":{"doc_sha":"ab774a7da8cfe9bf","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"move":{"doc_sha":"5c74f429970a2d2b","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"move(dx, dy) or (delta) -> None"},"name":{"doc_sha":"90a2ab0ec07c00eb","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"on_cell_click":{"doc_sha":"ada11ea920c71f29","kind":"property","lifecycle":{"since":"0.2.8-dev"},"signature":""},"on_cell_enter":{"doc_sha":"6ae7882d4f14f183","kind":"property","lifecycle":{"since":"0.2.8-dev"},"signature":""},"on_cell_exit":{"doc_sha":"729303b3a70b2a36","kind":"property","lifecycle":{"since":"0.2.8-dev"},"signature":""},"on_click":{"doc_sha":"4e33bd16b2dee20a","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"on_enter":{"doc_sha":"48c996802e262396","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"on_exit":{"doc_sha":"c14ef0d6d6d40b0e","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"on_move":{"doc_sha":"9c3e26818b5b25b0","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"opacity":{"doc_sha":"c1a7ef41e074519e","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"origin":{"doc_sha":"270bdf366c74bb1d","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"parent":{"doc_sha":"171b0de4e295c55b","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"perspective":{"doc_sha":"8854eef3677c4423","kind":"property","lifecycle":{"since":"0.2.8-dev"},"signature":""},"perspective_enabled":{"doc_sha":"abd2223cd551e1ca","kind":"property","lifecycle":{"since":"0.2.8-dev"},"signature":""},"pos":{"doc_sha":"7f556820c51df2c4","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"realign":{"doc_sha":"8dfa7333a49aa697","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"realign() -> None"},"resize":{"doc_sha":"005a04f1d129ebe5","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"resize(width, height) or (size) -> None"},"rotate_with_camera":{"doc_sha":"5c3af880218d3a08","kind":"property","lifecycle":{"modified":{"commit":"4bb1966","version":"0.2.8-dev"},"since":"0.2.8"},"signature":""},"rotation":{"doc_sha":"61ba3e66a0841a08","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"shader":{"doc_sha":"418952064319ffb5","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"size":{"doc_sha":"cfff2bccd482c2ef","kind":"property","lifecycle":{"since":"0.2.8-dev"},"signature":""},"texture":{"doc_sha":"b9bfb54e5571bfc9","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"uniforms":{"doc_sha":"b1f2699da618d002","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"vert_margin":{"doc_sha":"e2042be923bc6fcb","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"visible":{"doc_sha":"49221a4ae830f3b8","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"w":{"doc_sha":"ec55a5a578c4d993","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"x":{"doc_sha":"2449b0f04153753c","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"y":{"doc_sha":"7958c0625706f2d2","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"z_index":{"doc_sha":"0cb3df5278d2658b","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"zoom":{"doc_sha":"e56c4b61e0303f09","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""}}},"GridData":{"doc_sha":"793d86e4e51839f6","kind":"class","lifecycle":{"since":"0.2.8-dev"},"members":{"add_layer":{"doc_sha":"ad48373d656e6a88","kind":"method","lifecycle":{"since":"0.2.8-dev"},"signature":"add_layer(layer: ColorLayer | TileLayer) -> ColorLayer | TileLayer"},"apply_ranges":{"doc_sha":"f74ca906e65a4bec","kind":"method","lifecycle":{"since":"0.2.8-dev"},"signature":"apply_ranges(source: HeightMap, ranges: list) -> GridData"},"apply_threshold":{"doc_sha":"823c3779c6610389","kind":"method","lifecycle":{"since":"0.2.8-dev"},"signature":"apply_threshold(source: HeightMap, range: tuple, walkable: bool = None, transparent: bool = None) -> GridData"},"at":{"doc_sha":"0f12b1f70eaec14b","kind":"method","lifecycle":{"since":"0.2.8-dev"},"signature":"at(x: int, y: int) -> GridPoint"},"clear_dijkstra_maps":{"doc_sha":"d470811b59885b93","kind":"method","lifecycle":{"since":"0.2.8-dev"},"signature":"clear_dijkstra_maps() -> None"},"compute_fov":{"doc_sha":"2522197d48d43d98","kind":"method","lifecycle":{"since":"0.2.8-dev"},"signature":"compute_fov(pos, radius: int = 0, light_walls: bool = True, algorithm: FOV | int = FOV.BASIC) -> None"},"entities":{"doc_sha":"5c361e2150645749","kind":"property","lifecycle":{"since":"0.2.8-dev"},"signature":""},"entities_in_radius":{"doc_sha":"820857d7995c8fe6","kind":"method","lifecycle":{"since":"0.2.8-dev"},"signature":"entities_in_radius(pos: tuple | Vector, radius: float) -> list"},"find_path":{"doc_sha":"3de3f1a19ba24df8","kind":"method","lifecycle":{"since":"0.2.8-dev"},"signature":"find_path(start, end, diagonal_cost: float = 1.41, collide: str = None, heuristic = None, weight: float = 1.0) -> AStarPath | None"},"fov":{"doc_sha":"bb929647d575d246","kind":"property","lifecycle":{"since":"0.2.8-dev"},"signature":""},"fov_radius":{"doc_sha":"bf76a3199e6d7f0b","kind":"property","lifecycle":{"since":"0.2.8-dev"},"signature":""},"get_dijkstra_map":{"doc_sha":"af44def04f5c942e","kind":"method","lifecycle":{"since":"0.2.8-dev"},"signature":"get_dijkstra_map(root=None, diagonal_cost: float = 1.41, collide: str = None, roots=None) -> DijkstraMap"},"grid_h":{"doc_sha":"f96615d57a28b090","kind":"property","lifecycle":{"since":"0.2.8-dev"},"signature":""},"grid_size":{"doc_sha":"f1aff956f432c1fe","kind":"property","lifecycle":{"since":"0.2.8-dev"},"signature":""},"grid_w":{"doc_sha":"e2f7f55bad731ce6","kind":"property","lifecycle":{"since":"0.2.8-dev"},"signature":""},"is_in_fov":{"doc_sha":"3f871a007bbff04c","kind":"method","lifecycle":{"since":"0.2.8-dev"},"signature":"is_in_fov(x: int, y: int) -> bool"},"layer":{"doc_sha":"38d9c25b2b0b172b","kind":"method","lifecycle":{"since":"0.2.8-dev"},"signature":"layer(name: str) -> ColorLayer | TileLayer | None"},"layers":{"doc_sha":"9d6207bfc4909360","kind":"property","lifecycle":{"since":"0.2.8-dev"},"signature":""},"remove_layer":{"doc_sha":"7876426544d2ec0d","kind":"method","lifecycle":{"since":"0.2.8-dev"},"signature":"remove_layer(name_or_layer: str | ColorLayer | TileLayer) -> None"},"step":{"doc_sha":"38146d2fc84d70f3","kind":"method","lifecycle":{"since":"0.2.8-dev"},"signature":"step(n: int = 1, turn_order: int = None) -> None"},"texture":{"doc_sha":"a2911dd21b916bc0","kind":"property","lifecycle":{"since":"0.2.8-dev"},"signature":""}}},"GridView":{"doc_sha":"f88e43e992d43a86","kind":"class","lifecycle":{"since":"0.2.8"},"members":{"align":{"doc_sha":"58657c3cfa010f31","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"animate":{"doc_sha":"400df4f5893261c5","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"animate(property: str, target: Any, duration: float, easing=None, delta=False, loop=False, callback=None, conflict_mode='replace') -> Animation"},"bounds":{"doc_sha":"d3186ac3b3ff8bed","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"camera_rotation":{"doc_sha":"bc2cd502f52817e9","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"center":{"doc_sha":"913ef5c36eaf9635","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"center_camera":{"doc_sha":"00d37ccdedcd3a2e","kind":"method","lifecycle":{"since":"0.2.8-dev"},"signature":"center_camera(pos: tuple = None) -> None"},"center_x":{"doc_sha":"fbb0ff08116f3c80","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"center_y":{"doc_sha":"1bda27633025fb12","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"children":{"doc_sha":"9566d92d5619ee21","kind":"property","lifecycle":{"since":"0.2.8-dev"},"signature":""},"fill_color":{"doc_sha":"f26fb5da3de1789a","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"global_bounds":{"doc_sha":"aa4de9a20df55c2b","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"global_position":{"doc_sha":"b8dabbecc9fccd2a","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"grid_data":{"doc_sha":"fdf0084b8456fd45","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"h":{"doc_sha":"bd5a06618a50efad","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"horiz_margin":{"doc_sha":"0ef227a4cae6e675","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"hovered":{"doc_sha":"af0d353ce08f7be0","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"hovered_cell":{"doc_sha":"85c87c8aa013d0c4","kind":"property","lifecycle":{"since":"0.2.8-dev"},"signature":""},"margin":{"doc_sha":"ab774a7da8cfe9bf","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"move":{"doc_sha":"5c74f429970a2d2b","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"move(dx, dy) or (delta) -> None"},"name":{"doc_sha":"90a2ab0ec07c00eb","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"on_cell_click":{"doc_sha":"ada11ea920c71f29","kind":"property","lifecycle":{"since":"0.2.8-dev"},"signature":""},"on_cell_enter":{"doc_sha":"6ae7882d4f14f183","kind":"property","lifecycle":{"since":"0.2.8-dev"},"signature":""},"on_cell_exit":{"doc_sha":"729303b3a70b2a36","kind":"property","lifecycle":{"since":"0.2.8-dev"},"signature":""},"on_click":{"doc_sha":"4e33bd16b2dee20a","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"on_enter":{"doc_sha":"48c996802e262396","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"on_exit":{"doc_sha":"c14ef0d6d6d40b0e","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"on_move":{"doc_sha":"9c3e26818b5b25b0","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"opacity":{"doc_sha":"c1a7ef41e074519e","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"origin":{"doc_sha":"270bdf366c74bb1d","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"parent":{"doc_sha":"171b0de4e295c55b","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"perspective":{"doc_sha":"8854eef3677c4423","kind":"property","lifecycle":{"since":"0.2.8-dev"},"signature":""},"perspective_enabled":{"doc_sha":"abd2223cd551e1ca","kind":"property","lifecycle":{"since":"0.2.8-dev"},"signature":""},"pos":{"doc_sha":"7f556820c51df2c4","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"realign":{"doc_sha":"8dfa7333a49aa697","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"realign() -> None"},"resize":{"doc_sha":"005a04f1d129ebe5","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"resize(width, height) or (size) -> None"},"rotate_with_camera":{"doc_sha":"5c3af880218d3a08","kind":"property","lifecycle":{"modified":{"commit":"4bb1966","version":"0.2.8-dev"},"since":"0.2.8"},"signature":""},"rotation":{"doc_sha":"61ba3e66a0841a08","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"shader":{"doc_sha":"418952064319ffb5","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"size":{"doc_sha":"cfff2bccd482c2ef","kind":"property","lifecycle":{"since":"0.2.8-dev"},"signature":""},"texture":{"doc_sha":"b9bfb54e5571bfc9","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"uniforms":{"doc_sha":"b1f2699da618d002","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"vert_margin":{"doc_sha":"e2042be923bc6fcb","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"visible":{"doc_sha":"49221a4ae830f3b8","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"w":{"doc_sha":"ec55a5a578c4d993","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"x":{"doc_sha":"2449b0f04153753c","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"y":{"doc_sha":"7958c0625706f2d2","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"z_index":{"doc_sha":"0cb3df5278d2658b","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"zoom":{"doc_sha":"e56c4b61e0303f09","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""}}},"HeightMap":{"doc_sha":"e5f86ba9c89f2a5a","kind":"class","lifecycle":{"since":"0.2.8"},"members":{"add":{"doc_sha":"d420dd760d7228af","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"add(other: HeightMap, *, pos=None, source_pos=None, size=None) -> HeightMap"},"add_bsp":{"doc_sha":"b70f06e432c91a7a","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"add_bsp(bsp: BSP, *, pos=None, select: str = 'leaves', nodes: list = None, shrink: int = 0, value: float = 1.0) -> HeightMap"},"add_constant":{"doc_sha":"f382eb3f3e667cb4","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"add_constant(value: float, *, pos=None, size=None) -> HeightMap"},"add_hill":{"doc_sha":"1ad2406960701682","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"add_hill(center, radius: float, height: float) -> HeightMap"},"add_noise":{"doc_sha":"3c3c15026ff43959","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"add_noise(source: NoiseSource, world_origin: tuple = (0.0, 0.0), world_size: tuple = None, mode: str = 'fbm', octaves: int = 4, scale: float = 1.0) -> HeightMap"},"add_voronoi":{"doc_sha":"b689af51f6867b78","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"add_voronoi(num_points: int, coefficients: tuple = (1.0, -0.5), seed: int = None) -> HeightMap"},"clamp":{"doc_sha":"60c08680fb281402","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"clamp(min: float = 0.0, max: float = 1.0, *, pos=None, size=None) -> HeightMap"},"clear":{"doc_sha":"2297d98f231b6b7b","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"clear() -> HeightMap"},"copy_from":{"doc_sha":"80d64f3f6e76f8a7","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"copy_from(other: HeightMap, *, pos=None, source_pos=None, size=None) -> HeightMap"},"count_in_range":{"doc_sha":"7af3d60f28d614b6","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"count_in_range(range: tuple[float, float]) -> int"},"dig_bezier":{"doc_sha":"f3327d6ddc165496","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"dig_bezier(points: tuple, start_radius: float, end_radius: float, start_height: float, end_height: float) -> HeightMap"},"dig_hill":{"doc_sha":"be55e494c4e50c3a","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"dig_hill(center, radius: float, target_height: float) -> HeightMap"},"fill":{"doc_sha":"4e998a0b1d85c4f2","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"fill(value: float, *, pos=None, size=None) -> HeightMap"},"get":{"doc_sha":"f55fcc655d43dc50","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"get(x, y) or (pos) -> float"},"get_interpolated":{"doc_sha":"5c3e0267d9cbeda2","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"get_interpolated(x, y) or (pos) -> float"},"get_normal":{"doc_sha":"6d4212add4e533f2","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"get_normal(x, y, water_level=0.0) or (pos, water_level=0.0) -> tuple[float, float, float]"},"get_slope":{"doc_sha":"4876967f738c7d67","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"get_slope(x, y) or (pos) -> float"},"inverse":{"doc_sha":"f6c4de6cd6a9ca0f","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"inverse() -> HeightMap"},"lerp":{"doc_sha":"967cacc180df98bc","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"lerp(other: HeightMap, t: float, *, pos=None, source_pos=None, size=None) -> HeightMap"},"max":{"doc_sha":"141515f8b1f23390","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"max(other: HeightMap, *, pos=None, source_pos=None, size=None) -> HeightMap"},"mid_point_displacement":{"doc_sha":"bfcbfe796d73410f","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"mid_point_displacement(roughness: float = 0.5, seed: int = None) -> HeightMap"},"min":{"doc_sha":"c51e934ea5047d91","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"min(other: HeightMap, *, pos=None, source_pos=None, size=None) -> HeightMap"},"min_max":{"doc_sha":"4e0b3cb7472a7cfe","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"min_max() -> tuple[float, float]"},"multiply":{"doc_sha":"83d2fcea22463d18","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"multiply(other: HeightMap, *, pos=None, source_pos=None, size=None) -> HeightMap"},"multiply_bsp":{"doc_sha":"21f401c651344592","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"multiply_bsp(bsp: BSP, *, pos=None, select: str = 'leaves', nodes: list = None, shrink: int = 0, value: float = 1.0) -> HeightMap"},"multiply_noise":{"doc_sha":"177d1d06a401af54","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"multiply_noise(source: NoiseSource, world_origin: tuple = (0.0, 0.0), world_size: tuple = None, mode: str = 'fbm', octaves: int = 4, scale: float = 1.0) -> HeightMap"},"normalize":{"doc_sha":"93ce05bf80d158ed","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"normalize(min: float = 0.0, max: float = 1.0, *, pos=None, size=None) -> HeightMap"},"rain_erosion":{"doc_sha":"4dbe6bd42d9f6e5b","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"rain_erosion(drops: int, erosion: float = 0.1, sedimentation: float = 0.05, seed: int = None) -> HeightMap"},"scale":{"doc_sha":"c85d9324b84c43ff","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"scale(factor: float, *, pos=None, size=None) -> HeightMap"},"size":{"doc_sha":"31846e24f4028b35","kind":"property","lifecycle":{"since":"0.2.8"},"signature":"Dimensions (width, height) of the heightmap. Read-only."},"smooth":{"doc_sha":"243be30aa73da43b","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"smooth(iterations: int = 1) -> HeightMap"},"sparse_kernel":{"doc_sha":"e3b28d2c3913ac74","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"sparse_kernel(weights: dict[tuple[int, int], float]) -> HeightMap"},"sparse_kernel_from":{"doc_sha":"ebeed84050ee0b2f","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"sparse_kernel_from(source: HeightMap, weights: dict[tuple[int, int], float]) -> None"},"subtract":{"doc_sha":"8e209f74aaad794d","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"subtract(other: HeightMap, *, pos=None, source_pos=None, size=None) -> HeightMap"},"threshold":{"doc_sha":"946f6a1fb850decb","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"threshold(range: tuple[float, float]) -> HeightMap"},"threshold_binary":{"doc_sha":"6ebe7bd7cbc112c2","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"threshold_binary(range: tuple[float, float], value: float = 1.0) -> HeightMap"}}},"Heuristic":{"doc_sha":"7582db951ab67fc5","kind":"class","lifecycle":{"since":"0.2.8"},"members":{"as_integer_ratio":{"doc_sha":"3eb115fab0d0e0ec","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"($self, /)"},"bit_count":{"doc_sha":"f276da700b5a2e37","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"($self, /)"},"bit_length":{"doc_sha":"15b29db62fd0fa6c","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"($self, /)"},"conjugate":{"doc_sha":"6517a002faad7c75","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"($self, /)"},"denominator":{"doc_sha":"5051eb1570768851","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"from_bytes":{"doc_sha":"64672607865643a7","kind":"classmethod","lifecycle":{"since":"0.2.8"},"signature":"($type, /, bytes, byteorder='big', *, signed=False)"},"imag":{"doc_sha":"ef78ea410df39b26","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"is_integer":{"doc_sha":"6176763fbac2fd22","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"($self, /)"},"numerator":{"doc_sha":"ed255748ce2eda7c","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"real":{"doc_sha":"d44497f1990af6fe","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"to_bytes":{"doc_sha":"b452319e48e64764","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"($self, /, length=1, byteorder='big', *, signed=False)"}}},"InputState":{"doc_sha":"c7b7558edda29fea","kind":"class","lifecycle":{"since":"0.2.8"},"members":{"as_integer_ratio":{"doc_sha":"3eb115fab0d0e0ec","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"($self, /)"},"bit_count":{"doc_sha":"f276da700b5a2e37","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"($self, /)"},"bit_length":{"doc_sha":"15b29db62fd0fa6c","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"($self, /)"},"conjugate":{"doc_sha":"6517a002faad7c75","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"($self, /)"},"denominator":{"doc_sha":"5051eb1570768851","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"from_bytes":{"doc_sha":"64672607865643a7","kind":"classmethod","lifecycle":{"since":"0.2.8"},"signature":"($type, /, bytes, byteorder='big', *, signed=False)"},"imag":{"doc_sha":"ef78ea410df39b26","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"is_integer":{"doc_sha":"6176763fbac2fd22","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"($self, /)"},"numerator":{"doc_sha":"ed255748ce2eda7c","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"real":{"doc_sha":"d44497f1990af6fe","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"to_bytes":{"doc_sha":"b452319e48e64764","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"($self, /, length=1, byteorder='big', *, signed=False)"}}},"Key":{"doc_sha":"bc4feb60367e4be7","kind":"class","lifecycle":{"since":"0.2.8"},"members":{"as_integer_ratio":{"doc_sha":"3eb115fab0d0e0ec","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"($self, /)"},"bit_count":{"doc_sha":"f276da700b5a2e37","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"($self, /)"},"bit_length":{"doc_sha":"15b29db62fd0fa6c","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"($self, /)"},"conjugate":{"doc_sha":"6517a002faad7c75","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"($self, /)"},"denominator":{"doc_sha":"5051eb1570768851","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"from_bytes":{"doc_sha":"64672607865643a7","kind":"classmethod","lifecycle":{"since":"0.2.8"},"signature":"($type, /, bytes, byteorder='big', *, signed=False)"},"imag":{"doc_sha":"ef78ea410df39b26","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"is_integer":{"doc_sha":"6176763fbac2fd22","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"($self, /)"},"numerator":{"doc_sha":"ed255748ce2eda7c","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"real":{"doc_sha":"d44497f1990af6fe","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"to_bytes":{"doc_sha":"b452319e48e64764","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"($self, /, length=1, byteorder='big', *, signed=False)"}}},"Keyboard":{"doc_sha":"afbde6bcf7bfe1c6","kind":"class","lifecycle":{"since":"0.2.8"},"members":{"alt":{"doc_sha":"051cb7dbc470d328","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"ctrl":{"doc_sha":"c062912b0b816bd2","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"shift":{"doc_sha":"614beb0d432c2ec0","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"system":{"doc_sha":"2292efb47472f5e7","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""}}},"LdtkProject":{"doc_sha":"d80a5fdda811cb33","kind":"class","lifecycle":{"since":"0.2.8"},"members":{"enums":{"doc_sha":"5697f9e75980e2be","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"level":{"doc_sha":"33b80035be788d03","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"level(name: str) -> dict"},"level_names":{"doc_sha":"9479f6c2bd85608d","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"ruleset":{"doc_sha":"ce72d154e5dd391c","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"ruleset(name: str) -> AutoRuleSet"},"ruleset_names":{"doc_sha":"28983e9702e0dc81","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"tileset":{"doc_sha":"30eabe3003acb244","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"tileset(name: str) -> TileSetFile"},"tileset_names":{"doc_sha":"77a45b51f49743ef","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"version":{"doc_sha":"e6d3d49ad9edfad8","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""}}},"Line":{"doc_sha":"8d778faa93b5d0d5","kind":"class","lifecycle":{"since":"0.2.8"},"members":{"align":{"doc_sha":"58657c3cfa010f31","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"animate":{"doc_sha":"400df4f5893261c5","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"animate(property: str, target: Any, duration: float, easing=None, delta=False, loop=False, callback=None, conflict_mode='replace') -> Animation"},"bounds":{"doc_sha":"d3186ac3b3ff8bed","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"color":{"doc_sha":"f7aca0b82eaae97e","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"end":{"doc_sha":"cc14ff1baa1b61fa","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"global_bounds":{"doc_sha":"aa4de9a20df55c2b","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"global_position":{"doc_sha":"b8dabbecc9fccd2a","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"grid_pos":{"doc_sha":"9df77e9a996208ba","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"grid_size":{"doc_sha":"e32208817299dfe9","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"horiz_margin":{"doc_sha":"0ef227a4cae6e675","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"hovered":{"doc_sha":"af0d353ce08f7be0","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"margin":{"doc_sha":"ab774a7da8cfe9bf","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"move":{"doc_sha":"5c74f429970a2d2b","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"move(dx, dy) or (delta) -> None"},"name":{"doc_sha":"feeb7cb507a4692d","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"on_click":{"doc_sha":"49cecfb458b010a1","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"on_enter":{"doc_sha":"48c996802e262396","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"on_exit":{"doc_sha":"c14ef0d6d6d40b0e","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"on_move":{"doc_sha":"9c3e26818b5b25b0","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"opacity":{"doc_sha":"c1a7ef41e074519e","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"origin":{"doc_sha":"270bdf366c74bb1d","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"parent":{"doc_sha":"171b0de4e295c55b","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"pos":{"doc_sha":"733a48ea3a730e60","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"realign":{"doc_sha":"8dfa7333a49aa697","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"realign() -> None"},"resize":{"doc_sha":"005a04f1d129ebe5","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"resize(width, height) or (size) -> None"},"rotate_with_camera":{"doc_sha":"5c3af880218d3a08","kind":"property","lifecycle":{"modified":{"commit":"4bb1966","version":"0.2.8-dev"},"since":"0.2.8"},"signature":""},"rotation":{"doc_sha":"61ba3e66a0841a08","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"start":{"doc_sha":"817049419558ea17","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"thickness":{"doc_sha":"b6c7f3e979028709","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"vert_margin":{"doc_sha":"e2042be923bc6fcb","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"visible":{"doc_sha":"49221a4ae830f3b8","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"z_index":{"doc_sha":"cecb3704206d35f7","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""}}},"Model3D":{"doc_sha":"be7fb5003ed48d84","kind":"class","lifecycle":{"since":"0.2.8"},"members":{"animation_clips":{"doc_sha":"0d15ad9839919fe5","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"bone_count":{"doc_sha":"6e389fb2496946bf","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"bounds":{"doc_sha":"4ddfb8f6ae5a8928","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"cube":{"doc_sha":"b49160efa8b0f71e","kind":"classmethod","lifecycle":{"since":"0.2.8"},"signature":"cube(size=1.0) -> Model3D"},"has_skeleton":{"doc_sha":"4e9ac073dac101b6","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"mesh_count":{"doc_sha":"49f8fe0ece668d77","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"name":{"doc_sha":"b577d22f9a18de0c","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"plane":{"doc_sha":"5e31b94e295b4429","kind":"classmethod","lifecycle":{"since":"0.2.8"},"signature":"plane(width=1.0, depth=1.0, segments=1) -> Model3D"},"sphere":{"doc_sha":"b385d8aaf0efd485","kind":"classmethod","lifecycle":{"since":"0.2.8"},"signature":"sphere(radius=0.5, segments=16, rings=12) -> Model3D"},"triangle_count":{"doc_sha":"b67c98fac8ffc8dd","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"vertex_count":{"doc_sha":"8b76e70b26a820df","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""}}},"Mouse":{"doc_sha":"e64f561c8a08bdec","kind":"class","lifecycle":{"since":"0.2.8"},"members":{"grabbed":{"doc_sha":"3da420bb9cac434f","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"left":{"doc_sha":"a4c7b98babc093cb","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"middle":{"doc_sha":"1306b1240c6f64e3","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"pos":{"doc_sha":"2ecba0a68d64976a","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"right":{"doc_sha":"08d5534c1c034b39","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"visible":{"doc_sha":"834527eb5579f3e3","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"x":{"doc_sha":"c142f98ee94bf3f7","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"y":{"doc_sha":"5095db9e9e163e92","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""}}},"MouseButton":{"doc_sha":"a5a7584949d18f01","kind":"class","lifecycle":{"since":"0.2.8"},"members":{"as_integer_ratio":{"doc_sha":"3eb115fab0d0e0ec","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"($self, /)"},"bit_count":{"doc_sha":"f276da700b5a2e37","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"($self, /)"},"bit_length":{"doc_sha":"15b29db62fd0fa6c","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"($self, /)"},"conjugate":{"doc_sha":"6517a002faad7c75","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"($self, /)"},"denominator":{"doc_sha":"5051eb1570768851","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"from_bytes":{"doc_sha":"64672607865643a7","kind":"classmethod","lifecycle":{"since":"0.2.8"},"signature":"($type, /, bytes, byteorder='big', *, signed=False)"},"imag":{"doc_sha":"ef78ea410df39b26","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"is_integer":{"doc_sha":"6176763fbac2fd22","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"($self, /)"},"numerator":{"doc_sha":"ed255748ce2eda7c","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"real":{"doc_sha":"d44497f1990af6fe","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"to_bytes":{"doc_sha":"b452319e48e64764","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"($self, /, length=1, byteorder='big', *, signed=False)"}}},"Music":{"doc_sha":"e13e6ad378ec0e91","kind":"class","lifecycle":{"since":"0.2.8"},"members":{"duration":{"doc_sha":"25607c5fa852d501","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"loop":{"doc_sha":"a165a91f8ff771f5","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"pause":{"doc_sha":"4eb6d242151147dc","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"pause() -> None"},"play":{"doc_sha":"f724039e0cdb2847","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"play() -> None"},"playing":{"doc_sha":"4451d007747fa3b1","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"position":{"doc_sha":"608a5f6d4ed927bc","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"source":{"doc_sha":"ee5d7818bae0a255","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"stop":{"doc_sha":"f3289bbb98a4da1f","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"stop() -> None"},"volume":{"doc_sha":"749fc9d89252cb0e","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""}}},"NoiseSource":{"doc_sha":"3bfa3b53984c85cb","kind":"class","lifecycle":{"since":"0.2.8"},"members":{"algorithm":{"doc_sha":"ba813d6c022f15f0","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"dimensions":{"doc_sha":"f534d20928d8d67a","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"fbm":{"doc_sha":"d13c8670fc68b9df","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"fbm(pos: tuple[float, ...], octaves: int = 4) -> float"},"get":{"doc_sha":"3d7776fffd7a0a39","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"get(pos: tuple[float, ...]) -> float"},"hurst":{"doc_sha":"11682e6dc42f2417","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"lacunarity":{"doc_sha":"4d07f7079f3938de","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"sample":{"doc_sha":"63fd0e680000d0f0","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"sample(size: tuple[int, int], world_origin: tuple[float, float] = (0.0, 0.0), world_size: tuple[float, float] = None, mode: str = 'fbm', octaves: int = 4) -> HeightMap"},"seed":{"doc_sha":"17bc1e90a8ce6b23","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"turbulence":{"doc_sha":"800c841f913b3780","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"turbulence(pos: tuple[float, ...], octaves: int = 4) -> float"}}},"Perspective":{"doc_sha":"e1c8553ea2a32a40","kind":"class","lifecycle":{"since":"0.2.8"},"members":{"as_integer_ratio":{"doc_sha":"3eb115fab0d0e0ec","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"($self, /)"},"bit_count":{"doc_sha":"f276da700b5a2e37","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"($self, /)"},"bit_length":{"doc_sha":"15b29db62fd0fa6c","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"($self, /)"},"conjugate":{"doc_sha":"6517a002faad7c75","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"($self, /)"},"denominator":{"doc_sha":"5051eb1570768851","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"from_bytes":{"doc_sha":"64672607865643a7","kind":"classmethod","lifecycle":{"since":"0.2.8"},"signature":"($type, /, bytes, byteorder='big', *, signed=False)"},"imag":{"doc_sha":"ef78ea410df39b26","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"is_integer":{"doc_sha":"6176763fbac2fd22","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"($self, /)"},"numerator":{"doc_sha":"ed255748ce2eda7c","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"real":{"doc_sha":"d44497f1990af6fe","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"to_bytes":{"doc_sha":"b452319e48e64764","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"($self, /, length=1, byteorder='big', *, signed=False)"}}},"PropertyBinding":{"doc_sha":"a23eb43edd46bb66","kind":"class","lifecycle":{"since":"0.2.8"},"members":{"is_valid":{"doc_sha":"40ada14485c362a7","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"property":{"doc_sha":"0dad06f4e661db2d","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"target":{"doc_sha":"3b15e1922fd910c9","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"value":{"doc_sha":"80d28b4e329837d8","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""}}},"Scene":{"doc_sha":"a156c42d77b20a3e","kind":"class","lifecycle":{"since":"0.2.8"},"members":{"activate":{"doc_sha":"54ae3cd3cf652660","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"activate(transition: Transition = None, duration: float = None) -> None"},"active":{"doc_sha":"a59f70d3c12a7cfe","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"children":{"doc_sha":"f21fb480dac856b6","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"name":{"doc_sha":"7feed093cdf9334a","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"on_key":{"doc_sha":"40ece618402a45f3","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"opacity":{"doc_sha":"9e60a4ae9bdb493b","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"pos":{"doc_sha":"50e29b0dd34a76d5","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"realign":{"doc_sha":"eef1e2dce9ee8894","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"realign() -> None"},"register":{"doc_sha":"7603337801a7a498","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"register() -> None"},"registered":{"doc_sha":"d9ff54f488ed5b87","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"unregister":{"doc_sha":"fda6be9891d1f502","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"unregister() -> None"},"visible":{"doc_sha":"a7f321f4013dfb9b","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""}}},"Shader":{"doc_sha":"aded56cb37cba9d8","kind":"class","lifecycle":{"since":"0.2.8"},"members":{"dynamic":{"doc_sha":"43d8bc587c3997e3","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"is_valid":{"doc_sha":"fcdf95eedfa8479e","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"set_uniform":{"doc_sha":"bdaf877e5bc28f21","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"set_uniform(name: str, value: float|tuple) -> None"},"source":{"doc_sha":"dbd462698aadf450","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""}}},"Sound":{"doc_sha":"8f31cca1781580d5","kind":"class","lifecycle":{"since":"0.2.8"},"members":{"buffer":{"doc_sha":"c79f4e433d4300ac","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"duration":{"doc_sha":"48daac900b6eaf36","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"loop":{"doc_sha":"a624dff0e72a5337","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"pause":{"doc_sha":"005627aca21692d3","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"pause() -> None"},"pitch":{"doc_sha":"80b38bfc202b4291","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"play":{"doc_sha":"ae090ee8c0320797","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"play() -> None"},"play_varied":{"doc_sha":"fd9b5cd6db3cb1cf","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"play_varied(pitch_range: float = 0.1, volume_range: float = 3.0) -> None"},"playing":{"doc_sha":"8a0c491fa38061d5","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"source":{"doc_sha":"1736ca7393d96c48","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"stop":{"doc_sha":"f3289bbb98a4da1f","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"stop() -> None"},"volume":{"doc_sha":"749fc9d89252cb0e","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""}}},"SoundBuffer":{"doc_sha":"426618cde68541be","kind":"class","lifecycle":{"since":"0.2.8"},"members":{"bit_crush":{"doc_sha":"66ae55272fd9d182","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"bit_crush(bits: int, rate_divisor: int) -> SoundBuffer"},"channels":{"doc_sha":"f3948c1ceb5d7aec","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"concat":{"doc_sha":"7ee10bfe76aae5e2","kind":"classmethod","lifecycle":{"since":"0.2.8"},"signature":"concat(buffers: list[SoundBuffer], overlap: float = 0.0) -> SoundBuffer"},"distortion":{"doc_sha":"a36442ca725e5cc6","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"distortion(drive: float) -> SoundBuffer"},"duration":{"doc_sha":"bc1c03f781e0372b","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"echo":{"doc_sha":"1b93f79c671068ab","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"echo(delay_ms: float, feedback: float, wet: float) -> SoundBuffer"},"from_samples":{"doc_sha":"556326f1f2cda4a7","kind":"classmethod","lifecycle":{"since":"0.2.8"},"signature":"from_samples(data: bytes, channels: int, sample_rate: int) -> SoundBuffer"},"gain":{"doc_sha":"22fc3aad26b084a5","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"gain(factor: float) -> SoundBuffer"},"high_pass":{"doc_sha":"7d8af12720a4bbae","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"high_pass(cutoff_hz: float) -> SoundBuffer"},"low_pass":{"doc_sha":"82cdc215e7da9310","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"low_pass(cutoff_hz: float) -> SoundBuffer"},"mix":{"doc_sha":"4df14cf741427da9","kind":"classmethod","lifecycle":{"since":"0.2.8"},"signature":"mix(buffers: list[SoundBuffer]) -> SoundBuffer"},"normalize":{"doc_sha":"5476b16208b1ac7b","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"normalize() -> SoundBuffer"},"pitch_shift":{"doc_sha":"ee9373f2d71fdaa8","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"pitch_shift(factor: float) -> SoundBuffer"},"reverb":{"doc_sha":"b6d0dcf1e5f75a42","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"reverb(room_size: float, damping: float, wet: float) -> SoundBuffer"},"reverse":{"doc_sha":"eb70bfa5da4c860d","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"reverse() -> SoundBuffer"},"sample_count":{"doc_sha":"5e96b7263eade6af","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"sample_rate":{"doc_sha":"a41417a71e10a2d6","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"sfxr":{"doc_sha":"5f45a4b7afc74066","kind":"classmethod","lifecycle":{"since":"0.2.8"},"signature":"sfxr(preset: str = None, seed: int = None, **params) -> SoundBuffer"},"sfxr_mutate":{"doc_sha":"68b5b653de4f55dc","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"sfxr_mutate(amount: float = 0.05, seed: int = None) -> SoundBuffer"},"sfxr_params":{"doc_sha":"7fd7a2356600014a","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"slice":{"doc_sha":"84099d0f46e6b7ad","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"slice(start: float, end: float) -> SoundBuffer"},"tone":{"doc_sha":"eaec1748a9089774","kind":"classmethod","lifecycle":{"since":"0.2.8"},"signature":"tone(frequency: float, duration: float, waveform: str = 'sine', ...) -> SoundBuffer"}}},"Sprite":{"doc_sha":"a687c4f393725428","kind":"class","lifecycle":{"since":"0.2.8"},"members":{"align":{"doc_sha":"58657c3cfa010f31","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"animate":{"doc_sha":"400df4f5893261c5","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"animate(property: str, target: Any, duration: float, easing=None, delta=False, loop=False, callback=None, conflict_mode='replace') -> Animation"},"bounds":{"doc_sha":"d3186ac3b3ff8bed","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"global_bounds":{"doc_sha":"aa4de9a20df55c2b","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"global_position":{"doc_sha":"b8dabbecc9fccd2a","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"grid_pos":{"doc_sha":"9df77e9a996208ba","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"grid_size":{"doc_sha":"e32208817299dfe9","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"horiz_margin":{"doc_sha":"0ef227a4cae6e675","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"hovered":{"doc_sha":"af0d353ce08f7be0","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"margin":{"doc_sha":"ab774a7da8cfe9bf","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"move":{"doc_sha":"5c74f429970a2d2b","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"move(dx, dy) or (delta) -> None"},"name":{"doc_sha":"90a2ab0ec07c00eb","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"on_click":{"doc_sha":"b3b8de4e294d71b6","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"on_enter":{"doc_sha":"48c996802e262396","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"on_exit":{"doc_sha":"c14ef0d6d6d40b0e","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"on_move":{"doc_sha":"9c3e26818b5b25b0","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"opacity":{"doc_sha":"c1a7ef41e074519e","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"origin":{"doc_sha":"270bdf366c74bb1d","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"parent":{"doc_sha":"171b0de4e295c55b","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"pos":{"doc_sha":"55a118e7889fd555","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"realign":{"doc_sha":"8dfa7333a49aa697","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"realign() -> None"},"resize":{"doc_sha":"005a04f1d129ebe5","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"resize(width, height) or (size) -> None"},"rotate_with_camera":{"doc_sha":"5c3af880218d3a08","kind":"property","lifecycle":{"modified":{"commit":"4bb1966","version":"0.2.8-dev"},"since":"0.2.8"},"signature":""},"rotation":{"doc_sha":"61ba3e66a0841a08","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"scale":{"doc_sha":"f91a66d322ee1a75","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"scale_x":{"doc_sha":"95f47a809a8ac927","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"scale_y":{"doc_sha":"8567691c22a9b5f8","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"shader":{"doc_sha":"418952064319ffb5","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"sprite_index":{"doc_sha":"826af9fc52ae5cef","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"texture":{"doc_sha":"df3e7671926aa0a3","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"uniforms":{"doc_sha":"b1f2699da618d002","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"vert_margin":{"doc_sha":"e2042be923bc6fcb","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"visible":{"doc_sha":"49221a4ae830f3b8","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"x":{"doc_sha":"528487f8deaaf789","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"y":{"doc_sha":"114ecec940477107","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"z_index":{"doc_sha":"328d87ae44d9fb71","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""}}},"Texture":{"doc_sha":"da368b536d9f844e","kind":"class","lifecycle":{"since":"0.2.8"},"members":{"composite":{"doc_sha":"0bf406582f618af3","kind":"classmethod","lifecycle":{"since":"0.2.8"},"signature":"composite(layers: list[Texture], sprite_width: int, sprite_height: int, name: str = '') -> Texture"},"display_height":{"doc_sha":"0dc1ccea45ff4e40","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"display_offset_x":{"doc_sha":"0ca738fa6559aabb","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"display_offset_y":{"doc_sha":"83b5bfadd26f4e79","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"display_width":{"doc_sha":"cfc6525a8a0b5bb7","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"from_bytes":{"doc_sha":"979f5a384770d87f","kind":"classmethod","lifecycle":{"since":"0.2.8"},"signature":"from_bytes(data: bytes, width: int, height: int, sprite_width: int, sprite_height: int, name: str = '') -> Texture"},"hsl_shift":{"doc_sha":"95f19de7a67646e3","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"hsl_shift(hue_shift: float, sat_shift: float = 0.0, lit_shift: float = 0.0) -> Texture"},"sheet_height":{"doc_sha":"59d4fb71cae72fba","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"sheet_width":{"doc_sha":"54ef54eb9c39a907","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"source":{"doc_sha":"51bee1cdb17257ba","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"sprite_count":{"doc_sha":"d1ed79d8b59eca99","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"sprite_height":{"doc_sha":"19b6a36f45f965e8","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"sprite_width":{"doc_sha":"6f9276522c3b2512","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""}}},"TileLayer":{"doc_sha":"5551e3bf9e668d89","kind":"class","lifecycle":{"since":"0.2.8"},"members":{"apply_ranges":{"doc_sha":"e443e268532fdd2f","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"apply_ranges(source: HeightMap, ranges: list) -> TileLayer"},"apply_threshold":{"doc_sha":"2253f4367656d073","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"apply_threshold(source: HeightMap, range: tuple, tile: int) -> TileLayer"},"at":{"doc_sha":"19975fea19b3fadb","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"at(pos: tuple | Vector) or (x: int, y: int) -> int"},"edit":{"doc_sha":"47c32fda0e88780a","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"edit() -> context manager"},"fill":{"doc_sha":"5c8eca820794a605","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"fill(index: int) -> None"},"fill_rect":{"doc_sha":"adfc5ff6d6e91cc8","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"fill_rect(pos: tuple, size: tuple, index: int) -> None"},"grid":{"doc_sha":"b965c55d57de78fa","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"grid_size":{"doc_sha":"eac88eb0f7b16cbf","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"name":{"doc_sha":"dd282cc9273e2e36","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"set":{"doc_sha":"2b242556ce84080f","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"set(pos, index: int) -> None"},"texture":{"doc_sha":"1d211a219f88c72c","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"visible":{"doc_sha":"ecea81ec16e31c19","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"z_index":{"doc_sha":"eb13c1b4de11ce63","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""}}},"TileMapFile":{"doc_sha":"b26cb3870e41a2c9","kind":"class","lifecycle":{"since":"0.2.8"},"members":{"apply_to_tile_layer":{"doc_sha":"436e4fa972bd79d0","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"apply_to_tile_layer(tile_layer: TileLayer, layer_name: str, tileset_index: int = 0) -> None"},"height":{"doc_sha":"72aa81aefdab3ddb","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"object_layer":{"doc_sha":"e658613bcbbe9b3a","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"object_layer(name: str) -> list[dict]"},"object_layer_names":{"doc_sha":"7af1cc1454cd97f9","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"orientation":{"doc_sha":"2ee759a156b5c903","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"properties":{"doc_sha":"f2cb52eb132891d8","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"resolve_gid":{"doc_sha":"4a72f1c9f61cec27","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"resolve_gid(gid: int) -> tuple[int, int]"},"tile_height":{"doc_sha":"cc47760c48b8c9be","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"tile_layer_data":{"doc_sha":"399fd655e6402442","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"tile_layer_data(name: str) -> list[int]"},"tile_layer_names":{"doc_sha":"e225e2d4b71155e7","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"tile_width":{"doc_sha":"c109808c1e2d3ca1","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"tileset":{"doc_sha":"a3e35f368c3cc89d","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"tileset(index: int) -> tuple[int, TileSetFile]"},"tileset_count":{"doc_sha":"e7347387a4bcfeae","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"width":{"doc_sha":"acef1b7138a85569","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""}}},"TileSetFile":{"doc_sha":"ec83598f02a06258","kind":"class","lifecycle":{"since":"0.2.8"},"members":{"columns":{"doc_sha":"5ba187696ce9596e","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"image_source":{"doc_sha":"f21faacc0392eec3","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"margin":{"doc_sha":"6e1b3cfd5b59af6a","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"name":{"doc_sha":"5e0a76b40f4db151","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"properties":{"doc_sha":"e0382abd62d297a6","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"spacing":{"doc_sha":"950416fc0b8f1837","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"tile_count":{"doc_sha":"df5ccd0141400827","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"tile_height":{"doc_sha":"71a327fd061c1cf2","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"tile_info":{"doc_sha":"b33077f44c79e050","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"tile_info(tile_id: int) -> dict | None"},"tile_width":{"doc_sha":"888787b1ef69f5d9","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"to_texture":{"doc_sha":"610adac758051370","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"to_texture() -> Texture"},"wang_set":{"doc_sha":"c3b15db4611c250a","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"wang_set(name: str) -> WangSet"},"wang_sets":{"doc_sha":"71541a1c5777b29f","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""}}},"Timer":{"doc_sha":"646feff7bdb817f5","kind":"class","lifecycle":{"since":"0.2.8"},"members":{"active":{"doc_sha":"0ac0f4a607819264","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"callback":{"doc_sha":"a943e7e99b75fa22","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"interval":{"doc_sha":"e32658ee97f94823","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"name":{"doc_sha":"98a62482b59640f6","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"once":{"doc_sha":"7d5a7d20bae3793e","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"pause":{"doc_sha":"bfe544cc3c9f1cda","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"pause() -> None"},"paused":{"doc_sha":"060df10cbfc34662","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"remaining":{"doc_sha":"e223a64ed6a1ec63","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"restart":{"doc_sha":"a8c305ae5017cce0","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"restart() -> None"},"resume":{"doc_sha":"66ab0cd39f579ef9","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"resume() -> None"},"start":{"doc_sha":"0b2b51d812995d34","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"start() -> None"},"stop":{"doc_sha":"9f69f5e68512116c","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"stop() -> None"},"stopped":{"doc_sha":"bffdb8aa4589c28b","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""}}},"Transition":{"doc_sha":"e3b0c44298fc1c14","kind":"class","lifecycle":{"since":"0.2.8"},"members":{"as_integer_ratio":{"doc_sha":"3eb115fab0d0e0ec","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"($self, /)"},"bit_count":{"doc_sha":"f276da700b5a2e37","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"($self, /)"},"bit_length":{"doc_sha":"15b29db62fd0fa6c","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"($self, /)"},"conjugate":{"doc_sha":"6517a002faad7c75","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"($self, /)"},"denominator":{"doc_sha":"5051eb1570768851","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"from_bytes":{"doc_sha":"64672607865643a7","kind":"classmethod","lifecycle":{"since":"0.2.8"},"signature":"($type, /, bytes, byteorder='big', *, signed=False)"},"imag":{"doc_sha":"ef78ea410df39b26","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"is_integer":{"doc_sha":"6176763fbac2fd22","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"($self, /)"},"numerator":{"doc_sha":"ed255748ce2eda7c","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"real":{"doc_sha":"d44497f1990af6fe","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"to_bytes":{"doc_sha":"b452319e48e64764","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"($self, /, length=1, byteorder='big', *, signed=False)"}}},"Traversal":{"doc_sha":"e3b0c44298fc1c14","kind":"class","lifecycle":{"since":"0.2.8"},"members":{"as_integer_ratio":{"doc_sha":"3eb115fab0d0e0ec","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"($self, /)"},"bit_count":{"doc_sha":"f276da700b5a2e37","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"($self, /)"},"bit_length":{"doc_sha":"15b29db62fd0fa6c","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"($self, /)"},"conjugate":{"doc_sha":"6517a002faad7c75","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"($self, /)"},"denominator":{"doc_sha":"5051eb1570768851","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"from_bytes":{"doc_sha":"64672607865643a7","kind":"classmethod","lifecycle":{"since":"0.2.8"},"signature":"($type, /, bytes, byteorder='big', *, signed=False)"},"imag":{"doc_sha":"ef78ea410df39b26","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"is_integer":{"doc_sha":"6176763fbac2fd22","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"($self, /)"},"numerator":{"doc_sha":"ed255748ce2eda7c","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"real":{"doc_sha":"d44497f1990af6fe","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"to_bytes":{"doc_sha":"b452319e48e64764","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"($self, /, length=1, byteorder='big', *, signed=False)"}}},"Trigger":{"doc_sha":"3f698ad8b187be9f","kind":"class","lifecycle":{"since":"0.2.8"},"members":{"as_integer_ratio":{"doc_sha":"3eb115fab0d0e0ec","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"($self, /)"},"bit_count":{"doc_sha":"f276da700b5a2e37","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"($self, /)"},"bit_length":{"doc_sha":"15b29db62fd0fa6c","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"($self, /)"},"conjugate":{"doc_sha":"6517a002faad7c75","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"($self, /)"},"denominator":{"doc_sha":"5051eb1570768851","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"from_bytes":{"doc_sha":"64672607865643a7","kind":"classmethod","lifecycle":{"since":"0.2.8"},"signature":"($type, /, bytes, byteorder='big', *, signed=False)"},"imag":{"doc_sha":"ef78ea410df39b26","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"is_integer":{"doc_sha":"6176763fbac2fd22","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"($self, /)"},"numerator":{"doc_sha":"ed255748ce2eda7c","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"real":{"doc_sha":"d44497f1990af6fe","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"to_bytes":{"doc_sha":"b452319e48e64764","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"($self, /, length=1, byteorder='big', *, signed=False)"}}},"Vector":{"doc_sha":"0c8ae9e2ef387065","kind":"class","lifecycle":{"since":"0.2.8"},"members":{"angle":{"doc_sha":"a0ca5f1f4a89d780","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"angle() -> float"},"copy":{"doc_sha":"120e723fd513e072","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"copy() -> Vector"},"distance_to":{"doc_sha":"4f5d26bbe8434f65","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"distance_to(other: Vector) -> float"},"dot":{"doc_sha":"d69442ea289adfb8","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"dot(other: Vector) -> float"},"floor":{"doc_sha":"c7e81a90c5420968","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"floor() -> Vector"},"int":{"doc_sha":"3d97cae0ad8a963c","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"magnitude":{"doc_sha":"75f61a474afe3121","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"magnitude() -> float"},"magnitude_squared":{"doc_sha":"c976db5b540f827b","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"magnitude_squared() -> float"},"normalize":{"doc_sha":"04b468da3fc60858","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"normalize() -> Vector"},"x":{"doc_sha":"0e322bdd857e652b","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"y":{"doc_sha":"bbac84f15b6740d9","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""}}},"Viewport3D":{"doc_sha":"c41e58effff86261","kind":"class","lifecycle":{"since":"0.2.8"},"members":{"add_billboard":{"doc_sha":"ae2357f78a17a437","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"add_billboard(billboard)"},"add_layer":{"doc_sha":"b4c0274f9217aaa1","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"add_layer(name, z_index=0) -> dict"},"add_mesh":{"doc_sha":"588dbdf9a48f17e5","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"add_mesh(layer_name, model, pos, rotation=0, scale=1.0) -> int"},"add_voxel_layer":{"doc_sha":"47f17324425647e1","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"add_voxel_layer(voxel_grid, z_index=0)"},"animate":{"doc_sha":"400df4f5893261c5","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"animate(property: str, target: Any, duration: float, easing=None, delta=False, loop=False, callback=None, conflict_mode='replace') -> Animation"},"apply_heightmap":{"doc_sha":"be8e92a25726238d","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"apply_heightmap(heightmap, y_scale=1.0)"},"apply_terrain_colors":{"doc_sha":"cf0e47c185484e70","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"apply_terrain_colors(layer_name, r_map, g_map, b_map)"},"apply_threshold":{"doc_sha":"3adda69e435613d7","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"apply_threshold(heightmap, min_height, max_height, walkable=True)"},"at":{"doc_sha":"7ec76d7602627cb9","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"at(x, z) -> VoxelPoint"},"bg_color":{"doc_sha":"c7e7141bd1cadaea","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"billboard_count":{"doc_sha":"4b432c5c560ee44f","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"billboard_count() -> int"},"bounds":{"doc_sha":"d3186ac3b3ff8bed","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"build_terrain":{"doc_sha":"5be2a61f78907326","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"build_terrain(layer_name, heightmap, y_scale=1.0, cell_size=1.0) -> int"},"camera_pos":{"doc_sha":"7bfead8afc2b8312","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"camera_target":{"doc_sha":"3ecf7bb4dbdf27e4","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"cell_size":{"doc_sha":"8c3fa657c8fdc2aa","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"clear_billboards":{"doc_sha":"63d6d64c39b46264","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"clear_billboards()"},"clear_meshes":{"doc_sha":"1d6eb71f274020cf","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"clear_meshes(layer_name)"},"clear_voxel_nav_region":{"doc_sha":"56901862e4dc595a","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"clear_voxel_nav_region(voxel_grid)"},"compute_fov":{"doc_sha":"93c68ed57619564e","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"compute_fov(origin, radius=10) -> list"},"enable_affine":{"doc_sha":"96ba76ed43593940","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"enable_dither":{"doc_sha":"da6733dd08751b72","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"enable_fog":{"doc_sha":"ca6c5ffcef454868","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"enable_vertex_snap":{"doc_sha":"7401ad5dcd82024c","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"entities":{"doc_sha":"c2b185a3b718cc6b","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"find_path":{"doc_sha":"ebd174d40557cc19","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"find_path(start, end) -> list"},"fog_color":{"doc_sha":"3eb0cdbd75c5a94c","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"fog_far":{"doc_sha":"90406f8cae40bb58","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"fog_near":{"doc_sha":"e49f6dc79b22a794","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"follow":{"doc_sha":"975d2b945069be26","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"follow(entity, distance=10, height=5, smoothing=1.0)"},"fov":{"doc_sha":"7f5d69a1c423f69d","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"get_billboard":{"doc_sha":"38b0410480ab7d4d","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"get_billboard(index) -> Billboard"},"get_layer":{"doc_sha":"92223f274b5312c3","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"get_layer(name) -> dict or None"},"global_bounds":{"doc_sha":"aa4de9a20df55c2b","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"global_position":{"doc_sha":"b8dabbecc9fccd2a","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"grid_size":{"doc_sha":"7b4dbfa47f259f02","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"h":{"doc_sha":"d7308cd01fffb4be","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"hovered":{"doc_sha":"af0d353ce08f7be0","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"is_in_fov":{"doc_sha":"81d50fbbe2df4c8e","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"is_in_fov(x, z) -> bool"},"layer_count":{"doc_sha":"8bfbb28860b18d51","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"layer_count() -> int"},"move":{"doc_sha":"5c74f429970a2d2b","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"move(dx, dy) or (delta) -> None"},"on_click":{"doc_sha":"b3b8de4e294d71b6","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"on_enter":{"doc_sha":"48c996802e262396","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"on_exit":{"doc_sha":"c14ef0d6d6d40b0e","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"on_move":{"doc_sha":"9c3e26818b5b25b0","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"opacity":{"doc_sha":"c1a7ef41e074519e","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"orbit_camera":{"doc_sha":"29e02de46a996495","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"orbit_camera(angle=0, distance=10, height=5)"},"parent":{"doc_sha":"171b0de4e295c55b","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"place_blocking":{"doc_sha":"9252fd1d594a3867","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"place_blocking(grid_pos, footprint, walkable=False, transparent=False)"},"pos":{"doc_sha":"c1693d7dd0fe875d","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"project_all_voxels_to_nav":{"doc_sha":"ac9e0ca27b557d65","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"project_all_voxels_to_nav(headroom=2)"},"project_voxel_to_nav":{"doc_sha":"1fdf58bc686f7511","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"project_voxel_to_nav(voxel_grid, headroom=2)"},"realign":{"doc_sha":"8dfa7333a49aa697","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"realign() -> None"},"remove_billboard":{"doc_sha":"bf68668896f86bb0","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"remove_billboard(billboard)"},"remove_layer":{"doc_sha":"f5fafdfe3d153aab","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"remove_layer(name) -> bool"},"remove_voxel_layer":{"doc_sha":"b88a956cacc361cb","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"remove_voxel_layer(voxel_grid) -> bool"},"render_resolution":{"doc_sha":"4b141c5648005f04","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"resize":{"doc_sha":"005a04f1d129ebe5","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"resize(width, height) or (size) -> None"},"screen_to_world":{"doc_sha":"ba31510737afd516","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"screen_to_world(x, y, y_plane=0.0) -> tuple or None"},"set_grid_size":{"doc_sha":"ee7d1626547614c8","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"set_grid_size(width, depth)"},"set_slope_cost":{"doc_sha":"dac6d3cd319f1016","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"set_slope_cost(max_slope=0.5, cost_multiplier=1.0)"},"visible":{"doc_sha":"49221a4ae830f3b8","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"voxel_layer_count":{"doc_sha":"c1ca5c90881bbaae","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"voxel_layer_count() -> int"},"w":{"doc_sha":"4c5bcc34301022a0","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"x":{"doc_sha":"cd81d9dc457285aa","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"y":{"doc_sha":"406570900a1cf520","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"z_index":{"doc_sha":"328d87ae44d9fb71","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""}}},"VoxelGrid":{"doc_sha":"6706e3f5276ce104","kind":"class","lifecycle":{"since":"0.2.8"},"members":{"add_material":{"doc_sha":"27dec1c992cf7b99","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"add_material(name, color=Color(255,255,255), sprite_index=-1, transparent=False, path_cost=1.0) -> int"},"cell_size":{"doc_sha":"81b167d608b2cec5","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"clear":{"doc_sha":"8ebe20948a33a84d","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"clear() -> None"},"copy_region":{"doc_sha":"e56c5d28d7fd4e86","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"copy_region(min_coord, max_coord) -> VoxelRegion"},"count_material":{"doc_sha":"c9ef38e1f2f29ab8","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"count_material(material) -> int"},"count_non_air":{"doc_sha":"275d512b30f5134a","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"count_non_air() -> int"},"depth":{"doc_sha":"21671acfc312aeeb","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"fill":{"doc_sha":"fff2fcdddba5ee8c","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"fill(material) -> None"},"fill_box":{"doc_sha":"7c3d982a275f095d","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"fill_box(min_coord, max_coord, material) -> None"},"fill_box_hollow":{"doc_sha":"432e8b518feee0a5","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"fill_box_hollow(min_coord, max_coord, material, thickness=1) -> None"},"fill_cylinder":{"doc_sha":"0b2b0b0826381df0","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"fill_cylinder(base_pos, radius, height, material) -> None"},"fill_noise":{"doc_sha":"567355400611d920","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"fill_noise(min_coord, max_coord, material, threshold=0.5, scale=0.1, seed=0) -> None"},"fill_sphere":{"doc_sha":"e6a85d9c72e2ea44","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"fill_sphere(center, radius, material) -> None"},"from_bytes":{"doc_sha":"faefede0aa8ae636","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"from_bytes(data) -> bool"},"get":{"doc_sha":"52cea4645f0ea3d0","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"get(x, y, z) -> int"},"get_material":{"doc_sha":"2a61f6d54086df97","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"get_material(id) -> dict"},"greedy_meshing":{"doc_sha":"17fffa950c043917","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"height":{"doc_sha":"f271e989afe69da3","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"load":{"doc_sha":"5dcaf71057f836a2","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"load(path) -> bool"},"material_count":{"doc_sha":"0ef72bd68ef0c2d0","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"offset":{"doc_sha":"75230579ca06d8f5","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"paste_region":{"doc_sha":"1f44bacefd614cca","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"paste_region(region, position, skip_air=True) -> None"},"project_column":{"doc_sha":"9bacd59e3a7c5758","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"project_column(x, z, headroom=2) -> dict"},"rebuild_mesh":{"doc_sha":"fa60051037c3328c","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"rebuild_mesh() -> None"},"rotation":{"doc_sha":"5b603e8c652baf50","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"save":{"doc_sha":"bb7090694292f8e3","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"save(path) -> bool"},"set":{"doc_sha":"e9223501162aa9c6","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"set(x, y, z, material) -> None"},"size":{"doc_sha":"2d283d0584913b16","kind":"property","lifecycle":{"since":"0.2.8"},"signature":"Dimensions (width, height, depth) of the grid. Read-only."},"to_bytes":{"doc_sha":"f9a751e75bfc0478","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"to_bytes() -> bytes"},"vertex_count":{"doc_sha":"4df863704c32ecb7","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"visible":{"doc_sha":"7ff1afdbd4bf86a3","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"width":{"doc_sha":"0977aff160584918","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""}}},"VoxelRegion":{"doc_sha":"0cda0630ba7cafef","kind":"class","lifecycle":{"since":"0.2.8"},"members":{"depth":{"doc_sha":"05b3223f7df8ab65","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"height":{"doc_sha":"04834a15ca591472","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"size":{"doc_sha":"75c252385afbc498","kind":"property","lifecycle":{"since":"0.2.8"},"signature":"Dimensions (width, height, depth) of the region. Read-only."},"width":{"doc_sha":"d10d49e58037417d","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""}}},"WangSet":{"doc_sha":"320e161ef3d08a5d","kind":"class","lifecycle":{"since":"0.2.8"},"members":{"apply":{"doc_sha":"2aa8c0ba2d6549d2","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"apply(discrete_map: DiscreteMap, tile_layer: TileLayer) -> None"},"color_count":{"doc_sha":"13b244a538aec70f","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"colors":{"doc_sha":"6d84b3c2dd048b1a","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"name":{"doc_sha":"8b4560b705e5724f","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"resolve":{"doc_sha":"fe2fa09430c4bda6","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"resolve(discrete_map: DiscreteMap) -> list[int]"},"terrain_enum":{"doc_sha":"b8c46ec1a2b03eb6","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"terrain_enum() -> IntEnum"},"type":{"doc_sha":"ea3706e2c65ba7d3","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""}}},"Window":{"doc_sha":"6021e9b58544c378","kind":"class","lifecycle":{"since":"0.2.8"},"members":{"center":{"doc_sha":"91fe1b0d6ca2e4f1","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"center() -> None"},"framerate_limit":{"doc_sha":"de182ed271fbb4d3","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"fullscreen":{"doc_sha":"a517f5331026f2c4","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"game_resolution":{"doc_sha":"3a0dba59fecf5a95","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"get":{"doc_sha":"544bdc10ab93d067","kind":"classmethod","lifecycle":{"since":"0.2.8"},"signature":"get() -> Window"},"resolution":{"doc_sha":"548abee10c4fbcaf","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"scaling_mode":{"doc_sha":"57e03f9ffc9291ae","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"screenshot":{"doc_sha":"5102ab9c4c2c8358","kind":"method","lifecycle":{"since":"0.2.8"},"signature":"screenshot(filename: str = None) -> bytes | None"},"title":{"doc_sha":"0d98279e5ec1b8b1","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"visible":{"doc_sha":"bad7f81ec2e48a31","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""},"vsync":{"doc_sha":"0335eb1fba386e37","kind":"property","lifecycle":{"since":"0.2.8"},"signature":""}}}},"schema":1,"seeded":false,"version":"0.2.8-dev"} diff --git a/src/GameEngine.cpp b/src/GameEngine.cpp index 6658704..fc4c98d 100644 --- a/src/GameEngine.cpp +++ b/src/GameEngine.cpp @@ -257,12 +257,24 @@ void GameEngine::changeScene(std::string sceneName, TransitionType transitionTyp return; } - if (transitionType == TransitionType::None || duration <= 0.0f) + // #379: a transition needs something to transition FROM. Until the first Python + // scene is activated the engine sits on the internal "uitest" bootstrap scene, which + // is not a PyScene and has no Python wrapper -- so mcrfpy.current_scene reports None + // for it. Because current_scene reports the OUTGOING scene until a transition + // completes, fading from the bootstrap left current_scene as None for the whole + // transition, right after the user had assigned it. Nothing meaningful to transition + // from means change now. + const bool have_outgoing_scene = + !scene.empty() && + scenes.find(scene) != scenes.end() && + dynamic_cast(scenes[scene].get()) != nullptr; + + if (transitionType == TransitionType::None || duration <= 0.0f || !have_outgoing_scene) { // Immediate scene change std::string old_scene = scene; scene = sceneName; - + // Trigger Python scene lifecycle events McRFPy_API::triggerSceneChange(old_scene, sceneName); } diff --git a/tests/regression/issue_379_first_scene_transition_test.py b/tests/regression/issue_379_first_scene_transition_test.py new file mode 100644 index 0000000..5a68a33 --- /dev/null +++ b/tests/regression/issue_379_first_scene_transition_test.py @@ -0,0 +1,104 @@ +#!/usr/bin/env python3 +""" +Issue #379: a transition into the FIRST scene must not leave current_scene as None. + +While a transition runs, mcrfpy.current_scene reports the OUTGOING scene -- "current" +means "what is on screen". That is a sane convention for scene->scene. But at startup +there is no outgoing scene, so configuring default_transition before the first +activation made the engine fade from nothing, and current_scene read None for the +transition's entire duration: + + mcrfpy.default_transition = mcrfpy.Transition.FADE + mcrfpy.default_transition_duration = 1.0 + mcrfpy.current_scene = s + print(mcrfpy.current_scene) # None -- assigned one line ago + +A transition with nothing to transition from is not a transition. changeScene() now +takes the immediate path when there is no outgoing scene. + +Found by the tests/snippets/ gate: 062_scene_transition_fade.py and +063_scene_transition_slide.py both configure a transition and then activate their first +scene, which is the natural way to write that sample. +""" + +import sys + +import mcrfpy + +failures = [] + + +def check(cond, msg): + if not cond: + failures.append(msg) + print(f"FAIL: {msg}") + else: + print(f" ok: {msg}") + + +def frame(): + return mcrfpy.Frame(pos=(0, 0), size=(10, 10)) + + +def test_first_scene_with_transition_is_immediate(): + """The bug: no outgoing scene means nothing to fade from.""" + s = mcrfpy.Scene("first") + s.children.append(frame()) + + mcrfpy.default_transition = mcrfpy.Transition.FADE + mcrfpy.default_transition_duration = 1.0 + mcrfpy.current_scene = s + + got = mcrfpy.current_scene + check(got is not None, "current_scene is not None right after the first activation") + check(got is not None and got.name == "first", + f"current_scene is the scene we activated (got {got!r})") + + mcrfpy.step(0.016) + got = mcrfpy.current_scene + check(got is not None and got.name == "first", + "it is still the activated scene after a step") + + +def test_scene_to_scene_transition_still_defers(): + """The existing convention must not regress: mid-transition, current_scene is the + OUTGOING scene, and only becomes the incoming one on completion.""" + a = mcrfpy.Scene("from_a") + a.children.append(frame()) + b = mcrfpy.Scene("to_b") + b.children.append(frame()) + + mcrfpy.default_transition = mcrfpy.Transition.NONE + mcrfpy.default_transition_duration = 0.0 + mcrfpy.current_scene = a + check(mcrfpy.current_scene.name == "from_a", "scene a is active with no transition") + + mcrfpy.default_transition = mcrfpy.Transition.FADE + mcrfpy.default_transition_duration = 1.0 + mcrfpy.current_scene = b + + mcrfpy.step(0.1) + check(mcrfpy.current_scene.name == "from_a", + "mid-transition, current_scene is still the outgoing scene") + + for _ in range(20): + mcrfpy.step(0.1) + check(mcrfpy.current_scene.name == "to_b", + "after the transition completes, current_scene is the incoming scene") + + +def main(): + test_first_scene_with_transition_is_immediate() + test_scene_to_scene_transition_still_defers() + + if failures: + print(f"\nFAILED ({len(failures)} checks)") + for f in failures: + print(f" - {f}") + sys.exit(1) + print("\nPASS") + sys.exit(0) + + +if __name__ == "__main__": + main() diff --git a/tests/run_tests.py b/tests/run_tests.py index bdbea37..9d8b035 100644 --- a/tests/run_tests.py +++ b/tests/run_tests.py @@ -41,7 +41,17 @@ SANITIZER_PATTERNS = [ ] # Test directories to run (in order) -TEST_DIRS = ['unit', 'integration', 'regression', 'demo'] +TEST_DIRS = ['unit', 'integration', 'regression', 'demo', 'snippets'] + +# tests/snippets/ holds the code samples published on mcrogueface.github.io. They live +# here, in the engine repo, so that breaking the API breaks the build -- the docs site +# pulls them from here rather than keeping its own copy to rot in parallel. Before this, +# 130 snippets were executed by nothing at all and carried a hand-typed "status=ok" +# header that no run had ever verified. +# +# They are display scripts with no sys.exit(), because they must stay copy-pasteable, so +# _harness.py is chained as a second --exec to supply the ending (see its docstring). +SNIPPET_HARNESS = 'tests/snippets/_harness.py' # #372: tests/demo/ was never run by anything, so the demo screens -- which CLAUDE.md # points at as the canonical API-usage examples -- silently bitrotted until they could @@ -111,6 +121,10 @@ def run_test(test_path, verbose=False, timeout=DEFAULT_TIMEOUT, cmd.extend([str(MCROGUEFACE), '--headless', '--exec', str(test_path)]) + # A snippet has no ending of its own; chain the harness to supply one. + if test_path.parent.name == 'snippets': + cmd.extend(['--exec', str(TESTS_DIR / 'snippets' / '_harness.py')]) + try: result = subprocess.run( cmd, @@ -171,7 +185,8 @@ def find_tests(directory): if directory == 'demo': # #372: allowlisted entry points only -- see DEMO_SMOKE_TESTS. return [test_dir / name for name in DEMO_SMOKE_TESTS if (test_dir / name).exists()] - return sorted(test_dir.glob("*.py")) + # Leading underscore = support file, not a test (tests/snippets/_harness.py). + return sorted(p for p in test_dir.glob("*.py") if not p.name.startswith('_')) def main(): verbose = '-v' in sys.argv or '--verbose' in sys.argv diff --git a/tests/snippets/001_hello_frame.py b/tests/snippets/001_hello_frame.py new file mode 100644 index 0000000..3fcf09e --- /dev/null +++ b/tests/snippets/001_hello_frame.py @@ -0,0 +1,17 @@ +# mcrf: objects=[Color,Frame,Scene] verified=0.2.8-dev@a7ba486 status=ok +# Hello Frame - The simplest UI element +import mcrfpy + +# Create a scene and make it active +scene = mcrfpy.Scene("hello") +mcrfpy.current_scene = scene + +# Create a big, colorful frame in the center +frame = mcrfpy.Frame( + pos=(312, 234), + size=(400, 300), + fill_color=mcrfpy.Color(100, 150, 200), + outline_color=mcrfpy.Color(255, 255, 255), + outline=4.0 +) +scene.children.append(frame) diff --git a/tests/snippets/002_hello_caption.py b/tests/snippets/002_hello_caption.py new file mode 100644 index 0000000..94f2997 --- /dev/null +++ b/tests/snippets/002_hello_caption.py @@ -0,0 +1,16 @@ +# mcrf: objects=[Caption,Color,Scene] verified=0.2.8-dev@a7ba486 status=ok +# Hello Caption - Display text on screen +import mcrfpy + +scene = mcrfpy.Scene("hello") +mcrfpy.current_scene = scene + +# Large centered text +caption = mcrfpy.Caption( + text="Hello, McRogueFace!", + pos=(512, 350) +) +caption.fill_color = mcrfpy.Color(255, 220, 100) +# Center the text by adjusting position after creation +caption.x -= caption.w / 2 +scene.children.append(caption) diff --git a/tests/snippets/003_hello_sprite.py b/tests/snippets/003_hello_sprite.py new file mode 100644 index 0000000..cf057a1 --- /dev/null +++ b/tests/snippets/003_hello_sprite.py @@ -0,0 +1,19 @@ +# mcrf: objects=[Caption,Scene,Sprite] verified=0.2.8-dev@a7ba486 status=ok +# Hello Sprite - Display a textured sprite +import mcrfpy + +scene = mcrfpy.Scene("hello") +mcrfpy.current_scene = scene + +# Create a large sprite using default texture +sprite = mcrfpy.Sprite( + texture=mcrfpy.default_texture, + sprite_index=84, # Knight character + pos=(412, 284), + scale=12.0 +) +scene.children.append(sprite) + +# Add a label +label = mcrfpy.Caption(text="Sprite Index: 84", pos=(412, 550)) +scene.children.append(label) diff --git a/tests/snippets/004_hello_grid.py b/tests/snippets/004_hello_grid.py new file mode 100644 index 0000000..5c413ad --- /dev/null +++ b/tests/snippets/004_hello_grid.py @@ -0,0 +1,25 @@ +# mcrf: objects=[Grid,Scene] verified=0.2.8-dev@a7ba486 status=ok +# Hello Grid - Create a tile-based grid +import mcrfpy + +scene = mcrfpy.Scene("hello") +mcrfpy.current_scene = scene + +# Create a grid that fills the screen +# zoom=4.0 with 16x16 texture means each tile is 64x64 pixels +# 16 tiles * 64 pixels = 1024, 12 tiles * 64 pixels = 768 +grid = mcrfpy.Grid( + grid_size=(16, 12), + texture=mcrfpy.default_texture, + pos=(0, 0), + size=(1024, 768), + zoom=4.0 +) +scene.children.append(grid) + +# Fill with floor tiles +for y in range(12): + for x in range(16): + cell = grid.at(x, y) + cell.tilesprite = 48 # Floor tile + cell.walkable = True diff --git a/tests/snippets/005_hello_entity.py b/tests/snippets/005_hello_entity.py new file mode 100644 index 0000000..7bedb23 --- /dev/null +++ b/tests/snippets/005_hello_entity.py @@ -0,0 +1,29 @@ +# mcrf: objects=[Entity,Grid,Scene] verified=0.2.8-dev@a7ba486 status=ok +# Hello Entity - Add entities to a grid +import mcrfpy + +scene = mcrfpy.Scene("hello") +mcrfpy.current_scene = scene + +# Create grid filling the screen +grid = mcrfpy.Grid( + grid_size=(16, 12), + texture=mcrfpy.default_texture, + pos=(0, 0), + size=(1024, 768), + zoom=4.0 +) +scene.children.append(grid) + +# Floor tiles +for y in range(12): + for x in range(16): + grid.at(x, y).tilesprite = 48 + +# Add a player entity +player = mcrfpy.Entity( + grid_pos=(8, 6), + texture=mcrfpy.default_texture, + sprite_index=84 # Knight +) +grid.entities.append(player) diff --git a/tests/snippets/006_frame_colors.py b/tests/snippets/006_frame_colors.py new file mode 100644 index 0000000..8d07637 --- /dev/null +++ b/tests/snippets/006_frame_colors.py @@ -0,0 +1,33 @@ +# mcrf: objects=[Caption,Color,Frame,Scene] verified=0.2.8-dev@a7ba486 status=ok +# Frame Colors - Fill and outline colors +import mcrfpy + +scene = mcrfpy.Scene("demo") +mcrfpy.current_scene = scene + +# Dark background +scene.children.append(mcrfpy.Frame(pos=(0, 0), size=(1024, 768), fill_color=mcrfpy.Color(30, 30, 40))) + +colors = [ + (mcrfpy.Color(255, 100, 100), "Red"), + (mcrfpy.Color(100, 255, 100), "Green"), + (mcrfpy.Color(100, 100, 255), "Blue"), + (mcrfpy.Color(255, 255, 100), "Yellow"), +] + +for i, (color, name) in enumerate(colors): + x = 112 + (i % 2) * 420 + y = 134 + (i // 2) * 280 + + frame = mcrfpy.Frame( + pos=(x, y), size=(380, 240), + fill_color=color, + outline_color=mcrfpy.Color(255, 255, 255), + outline=3.0 + ) + scene.children.append(frame) + + label = mcrfpy.Caption(text=name, pos=(x + 150, y + 100)) + label.outline = 2 + label.outline_color = mcrfpy.Color(0, 0, 0) + scene.children.append(label) diff --git a/tests/snippets/007_frame_opacity.py b/tests/snippets/007_frame_opacity.py new file mode 100644 index 0000000..97399c8 --- /dev/null +++ b/tests/snippets/007_frame_opacity.py @@ -0,0 +1,34 @@ +# mcrf: objects=[Caption,Color,Frame,Scene] verified=0.2.8-dev@a7ba486 status=ok +# Frame Opacity - Transparency levels +import mcrfpy + +scene = mcrfpy.Scene("demo") +mcrfpy.current_scene = scene + +# Background pattern +for i in range(20): + stripe = mcrfpy.Frame( + pos=(i * 52, 0), size=(26, 768), + fill_color=mcrfpy.Color(50, 50, 80) + ) + scene.children.append(stripe) + +# Frames with varying opacity +opacities = [1.0, 0.75, 0.5, 0.25] +for i, opacity in enumerate(opacities): + frame = mcrfpy.Frame( + pos=(112 + i * 220, 234), size=(180, 300), + fill_color=mcrfpy.Color(255, 150, 50), + outline=2.0, + outline_color=mcrfpy.Color(255, 255, 255), + opacity=opacity + ) + scene.children.append(frame) + + label = mcrfpy.Caption( + text=f"opacity={opacity}", + pos=(112 + i * 220 + 30, 550) + ) + label.outline = 2 + label.outline_color = mcrfpy.Color(0, 0, 0) + scene.children.append(label) diff --git a/tests/snippets/008_frame_outline.py b/tests/snippets/008_frame_outline.py new file mode 100644 index 0000000..cf79631 --- /dev/null +++ b/tests/snippets/008_frame_outline.py @@ -0,0 +1,22 @@ +# mcrf: objects=[Caption,Color,Frame,Scene] verified=0.2.8-dev@a7ba486 status=ok +# Frame Outline - Border thickness +import mcrfpy + +scene = mcrfpy.Scene("demo") +mcrfpy.current_scene = scene + +outlines = [0, 2, 5, 10, 20] +for i, thickness in enumerate(outlines): + frame = mcrfpy.Frame( + pos=(92 + i * 180, 234), size=(160, 300), + fill_color=mcrfpy.Color(60, 80, 120), + outline_color=mcrfpy.Color(255, 200, 100), + outline=thickness + ) + scene.children.append(frame) + + label = mcrfpy.Caption( + text=f"outline={thickness}", + pos=(92 + i * 180 + 30, 550) + ) + scene.children.append(label) diff --git a/tests/snippets/009_frame_children.py b/tests/snippets/009_frame_children.py new file mode 100644 index 0000000..deff54f --- /dev/null +++ b/tests/snippets/009_frame_children.py @@ -0,0 +1,41 @@ +# mcrf: objects=[Caption,Color,Frame,Scene] verified=0.2.8-dev@a7ba486 status=ok +# Frame Children - Nested UI elements +import mcrfpy + +scene = mcrfpy.Scene("demo") +mcrfpy.current_scene = scene + +# Parent container +parent = mcrfpy.Frame( + pos=(162, 134), size=(700, 500), + fill_color=mcrfpy.Color(40, 40, 60), + outline=3.0, + outline_color=mcrfpy.Color(100, 100, 150) +) +scene.children.append(parent) + +# Child frames - positions relative to parent +child1 = mcrfpy.Frame( + pos=(20, 20), size=(200, 200), + fill_color=mcrfpy.Color(200, 80, 80) +) +parent.children.append(child1) + +child2 = mcrfpy.Frame( + pos=(240, 20), size=(200, 200), + fill_color=mcrfpy.Color(80, 200, 80) +) +parent.children.append(child2) + +child3 = mcrfpy.Frame( + pos=(460, 20), size=(200, 200), + fill_color=mcrfpy.Color(80, 80, 200) +) +parent.children.append(child3) + +# Label +label = mcrfpy.Caption( + text="Children use parent-relative coordinates", + pos=(150, 450) +) +parent.children.append(label) diff --git a/tests/snippets/010_frame_clip_children.py b/tests/snippets/010_frame_clip_children.py new file mode 100644 index 0000000..9cc812e --- /dev/null +++ b/tests/snippets/010_frame_clip_children.py @@ -0,0 +1,42 @@ +# mcrf: objects=[Caption,Color,Frame,Scene] verified=0.2.8-dev@a7ba486 status=ok +# Frame Clipping - clip_children property +import mcrfpy + +scene = mcrfpy.Scene("demo") +mcrfpy.current_scene = scene + +# Frame WITHOUT clipping +no_clip = mcrfpy.Frame( + pos=(100, 200), size=(300, 300), + fill_color=mcrfpy.Color(60, 60, 100), + outline=2.0, outline_color=mcrfpy.Color(150, 150, 200), + clip_children=False +) +scene.children.append(no_clip) + +overflow1 = mcrfpy.Frame( + pos=(150, 150), size=(200, 200), + fill_color=mcrfpy.Color(255, 100, 100) +) +no_clip.children.append(overflow1) + +label1 = mcrfpy.Caption(text="clip_children=False", pos=(120, 520)) +scene.children.append(label1) + +# Frame WITH clipping +with_clip = mcrfpy.Frame( + pos=(524, 200), size=(300, 300), + fill_color=mcrfpy.Color(60, 60, 100), + outline=2.0, outline_color=mcrfpy.Color(150, 150, 200), + clip_children=True +) +scene.children.append(with_clip) + +overflow2 = mcrfpy.Frame( + pos=(150, 150), size=(200, 200), + fill_color=mcrfpy.Color(100, 255, 100) +) +with_clip.children.append(overflow2) + +label2 = mcrfpy.Caption(text="clip_children=True", pos=(544, 520)) +scene.children.append(label2) diff --git a/tests/snippets/011_frame_z_index.py b/tests/snippets/011_frame_z_index.py new file mode 100644 index 0000000..14f4a83 --- /dev/null +++ b/tests/snippets/011_frame_z_index.py @@ -0,0 +1,48 @@ +# mcrf: objects=[Caption,Color,Frame,Scene] verified=0.2.8-dev@a7ba486 status=ok +# Frame Z-Index - Control rendering order +import mcrfpy + +scene = mcrfpy.Scene("demo") +mcrfpy.current_scene = scene + +# Dark background +scene.children.append(mcrfpy.Frame(pos=(0, 0), size=(1024, 768), fill_color=mcrfpy.Color(30, 30, 40))) + +# Create overlapping frames with different z_index +# Lower z_index = rendered first (behind) +back = mcrfpy.Frame( + pos=(262, 184), size=(300, 300), + fill_color=mcrfpy.Color(255, 100, 100), + z_index=0 +) +scene.children.append(back) + +middle = mcrfpy.Frame( + pos=(362, 234), size=(300, 300), + fill_color=mcrfpy.Color(100, 255, 100), + z_index=1 +) +scene.children.append(middle) + +front = mcrfpy.Frame( + pos=(462, 284), size=(300, 300), + fill_color=mcrfpy.Color(100, 100, 255), + z_index=2 +) +scene.children.append(front) + +# Labels with outlines for visibility +label1 = mcrfpy.Caption(text="z_index=0 (back)", pos=(262, 500)) +label1.outline = 2 +label1.outline_color = mcrfpy.Color(0, 0, 0) +scene.children.append(label1) + +label2 = mcrfpy.Caption(text="z_index=1 (middle)", pos=(362, 550)) +label2.outline = 2 +label2.outline_color = mcrfpy.Color(0, 0, 0) +scene.children.append(label2) + +label3 = mcrfpy.Caption(text="z_index=2 (front)", pos=(462, 600)) +label3.outline = 2 +label3.outline_color = mcrfpy.Color(0, 0, 0) +scene.children.append(label3) diff --git a/tests/snippets/012_frame_visible.py b/tests/snippets/012_frame_visible.py new file mode 100644 index 0000000..77c1a7d --- /dev/null +++ b/tests/snippets/012_frame_visible.py @@ -0,0 +1,32 @@ +# mcrf: objects=[Caption,Color,Frame,Scene] verified=0.2.8-dev@a7ba486 status=ok +# Frame Visibility - Show and hide elements +import mcrfpy + +scene = mcrfpy.Scene("demo") +mcrfpy.current_scene = scene + +# Create three frames +visible_frame = mcrfpy.Frame( + pos=(100, 234), size=(250, 300), + fill_color=mcrfpy.Color(100, 200, 100), + visible=True +) +scene.children.append(visible_frame) +scene.children.append(mcrfpy.Caption(text="visible=True", pos=(140, 550))) + +hidden_frame = mcrfpy.Frame( + pos=(387, 234), size=(250, 300), + fill_color=mcrfpy.Color(200, 100, 100), + visible=False # This won't be displayed +) +scene.children.append(hidden_frame) +scene.children.append(mcrfpy.Caption(text="visible=False", pos=(427, 550))) +scene.children.append(mcrfpy.Caption(text="(frame exists but hidden)", pos=(397, 580))) + +visible_frame2 = mcrfpy.Frame( + pos=(674, 234), size=(250, 300), + fill_color=mcrfpy.Color(100, 100, 200), + visible=True +) +scene.children.append(visible_frame2) +scene.children.append(mcrfpy.Caption(text="visible=True", pos=(714, 550))) diff --git a/tests/snippets/013_frame_name.py b/tests/snippets/013_frame_name.py new file mode 100644 index 0000000..5d313f5 --- /dev/null +++ b/tests/snippets/013_frame_name.py @@ -0,0 +1,41 @@ +# mcrf: objects=[Caption,Color,Frame,Scene] verified=0.2.8-dev@a7ba486 status=ok +# Frame Name - Named elements for lookup +import mcrfpy + +scene = mcrfpy.Scene("demo") +mcrfpy.current_scene = scene + +# Create named frames +header = mcrfpy.Frame( + pos=(112, 50), size=(800, 100), + fill_color=mcrfpy.Color(80, 80, 120), + name="header" +) +scene.children.append(header) + +sidebar = mcrfpy.Frame( + pos=(112, 170), size=(200, 500), + fill_color=mcrfpy.Color(60, 100, 80), + name="sidebar" +) +scene.children.append(sidebar) + +content = mcrfpy.Frame( + pos=(332, 170), size=(580, 500), + fill_color=mcrfpy.Color(100, 80, 60), + name="main_content" +) +scene.children.append(content) + +# Find elements by name +found = mcrfpy.find("header") +if found: + label = mcrfpy.Caption( + text=f"Found: '{found.name}'", + pos=(20, 30) + ) + found.children.append(label) + +# Display all names +scene.children.append(mcrfpy.Caption(text="sidebar", pos=(170, 400))) +scene.children.append(mcrfpy.Caption(text="main_content", pos=(550, 400))) diff --git a/tests/snippets/014_frame_rotation.py b/tests/snippets/014_frame_rotation.py new file mode 100644 index 0000000..3ebcb51 --- /dev/null +++ b/tests/snippets/014_frame_rotation.py @@ -0,0 +1,29 @@ +# mcrf: objects=[Caption,Color,Frame,Scene] verified=0.2.8-dev@a7ba486 status=ok +# Frame Rotation - Rotate UI elements +import mcrfpy + +scene = mcrfpy.Scene("demo") +mcrfpy.current_scene = scene + +# Dark background +scene.children.append(mcrfpy.Frame(pos=(0, 0), size=(1024, 768), fill_color=mcrfpy.Color(30, 30, 40))) + +rotations = [0, 15, 30, 45, 60] +for i, angle in enumerate(rotations): + frame = mcrfpy.Frame( + pos=(100 + i * 180, 300), size=(120, 120), + fill_color=mcrfpy.Color(150, 100, 200), + outline=3.0, + outline_color=mcrfpy.Color(255, 255, 255) + ) + frame.rotation = angle + scene.children.append(frame) + + label = mcrfpy.Caption(text=f"{angle}deg", pos=(130 + i * 180, 500)) + label.outline = 2 + label.outline_color = mcrfpy.Color(0, 0, 0) + scene.children.append(label) + +title = mcrfpy.Caption(text="Frame Rotation", pos=(420, 100)) +title.fill_color = mcrfpy.Color(255, 220, 100) +scene.children.append(title) diff --git a/tests/snippets/015_frame_origin.py b/tests/snippets/015_frame_origin.py new file mode 100644 index 0000000..0ada55d --- /dev/null +++ b/tests/snippets/015_frame_origin.py @@ -0,0 +1,45 @@ +# mcrf: objects=[Caption,Color,Frame,Scene,Vector] verified=0.2.8-dev@a7ba486 status=ok +# Frame Origin - Rotation pivot point +import mcrfpy + +scene = mcrfpy.Scene("demo") +mcrfpy.current_scene = scene + +# Dark background +scene.children.append(mcrfpy.Frame(pos=(0, 0), size=(1024, 768), fill_color=mcrfpy.Color(30, 30, 40))) + +# All frames rotated 45 degrees, different origins +origins = [ + ((0, 0), "top-left"), + ((60, 60), "center"), + ((120, 0), "top-right"), + ((0, 120), "bottom-left"), +] + +for i, (origin, name) in enumerate(origins): + x = 150 + (i % 2) * 400 + y = 180 + (i // 2) * 280 + + # Reference frame (no rotation) + ref = mcrfpy.Frame( + pos=(x, y), size=(120, 120), + fill_color=mcrfpy.Color(50, 50, 50), + outline=1.0, + outline_color=mcrfpy.Color(100, 100, 100) + ) + scene.children.append(ref) + + # Rotated frame + frame = mcrfpy.Frame( + pos=(x, y), size=(120, 120), + fill_color=mcrfpy.Color(200, 100, 150), + opacity=0.8 + ) + frame.rotation = 45 + frame.origin = mcrfpy.Vector(origin[0], origin[1]) + scene.children.append(frame) + + label = mcrfpy.Caption(text=f"origin: {name}", pos=(x, y + 160)) + label.outline = 2 + label.outline_color = mcrfpy.Color(0, 0, 0) + scene.children.append(label) diff --git a/tests/snippets/016_frame_alignment.py b/tests/snippets/016_frame_alignment.py new file mode 100644 index 0000000..949edba --- /dev/null +++ b/tests/snippets/016_frame_alignment.py @@ -0,0 +1,43 @@ +# mcrf: objects=[Alignment,Color,Frame,Scene] verified=0.2.8-dev@a7ba486 status=ok +# Frame Alignment - Automatic positioning +import mcrfpy + +scene = mcrfpy.Scene("demo") +mcrfpy.current_scene = scene + +# Container to show alignment +container = mcrfpy.Frame( + pos=(112, 84), size=(800, 600), + fill_color=mcrfpy.Color(40, 40, 60), + outline=2.0 +) +scene.children.append(container) + +# Grid of alignment options (excluding CENTER to avoid margin error) +alignments = [ + (mcrfpy.Alignment.TOP_LEFT, "TOP_LEFT"), + (mcrfpy.Alignment.TOP_CENTER, "TOP_CENTER"), + (mcrfpy.Alignment.TOP_RIGHT, "TOP_RIGHT"), + (mcrfpy.Alignment.CENTER_LEFT, "CENTER_LEFT"), + (mcrfpy.Alignment.CENTER_RIGHT, "CENTER_RIGHT"), + (mcrfpy.Alignment.BOTTOM_LEFT, "BOTTOM_LEFT"), + (mcrfpy.Alignment.BOTTOM_CENTER, "BOTTOM_CENTER"), + (mcrfpy.Alignment.BOTTOM_RIGHT, "BOTTOM_RIGHT"), +] + +for alignment, name in alignments: + box = mcrfpy.Frame( + size=(80, 60), + fill_color=mcrfpy.Color(150, 100, 200), + align=alignment, + margin=10 + ) + container.children.append(box) + +# Center box (no margin for CENTER) +center_box = mcrfpy.Frame( + size=(80, 60), + fill_color=mcrfpy.Color(100, 200, 150), + align=mcrfpy.Alignment.CENTER +) +container.children.append(center_box) diff --git a/tests/snippets/017_caption_styling.py b/tests/snippets/017_caption_styling.py new file mode 100644 index 0000000..2df4d62 --- /dev/null +++ b/tests/snippets/017_caption_styling.py @@ -0,0 +1,26 @@ +# mcrf: objects=[Caption,Color,Scene] verified=0.2.8-dev@a7ba486 status=ok +# Caption Styling - Text colors and fonts +import mcrfpy + +scene = mcrfpy.Scene("demo") +mcrfpy.current_scene = scene + +# Title +title = mcrfpy.Caption(text="Caption Styles", pos=(350, 80)) +title.fill_color = mcrfpy.Color(255, 255, 255) +scene.children.append(title) + +# Different colors +colors = [ + (mcrfpy.Color(255, 100, 100), "Red Text"), + (mcrfpy.Color(100, 255, 100), "Green Text"), + (mcrfpy.Color(100, 100, 255), "Blue Text"), + (mcrfpy.Color(255, 255, 100), "Yellow Text"), + (mcrfpy.Color(255, 100, 255), "Magenta Text"), + (mcrfpy.Color(100, 255, 255), "Cyan Text"), +] + +for i, (color, text) in enumerate(colors): + cap = mcrfpy.Caption(text=text, pos=(350, 180 + i * 80)) + cap.fill_color = color + scene.children.append(cap) diff --git a/tests/snippets/018_caption_in_frame.py b/tests/snippets/018_caption_in_frame.py new file mode 100644 index 0000000..22bef59 --- /dev/null +++ b/tests/snippets/018_caption_in_frame.py @@ -0,0 +1,34 @@ +# mcrf: objects=[Caption,Color,Frame,Scene] verified=0.2.8-dev@a7ba486 status=ok +# Caption in Frame - Text inside containers +import mcrfpy + +scene = mcrfpy.Scene("demo") +mcrfpy.current_scene = scene + +# Card-style container with text +card = mcrfpy.Frame( + pos=(262, 184), size=(500, 400), + fill_color=mcrfpy.Color(50, 60, 80), + outline=3.0, + outline_color=mcrfpy.Color(100, 120, 160) +) +scene.children.append(card) + +# Title in card +card_title = mcrfpy.Caption(text="Player Stats", pos=(180, 30)) +card_title.fill_color = mcrfpy.Color(255, 220, 100) +card.children.append(card_title) + +# Stats +stats = [ + "Health: 100/100", + "Mana: 50/50", + "Strength: 15", + "Defense: 12", + "Speed: 18", +] + +for i, stat in enumerate(stats): + line = mcrfpy.Caption(text=stat, pos=(50, 100 + i * 50)) + line.fill_color = mcrfpy.Color(200, 200, 200) + card.children.append(line) diff --git a/tests/snippets/019_caption_dimensions.py b/tests/snippets/019_caption_dimensions.py new file mode 100644 index 0000000..d2e72e7 --- /dev/null +++ b/tests/snippets/019_caption_dimensions.py @@ -0,0 +1,36 @@ +# mcrf: objects=[Caption,Color,Frame,Scene] verified=0.2.8-dev@a7ba486 status=ok +# Caption Dimensions - Text width and height +import mcrfpy + +scene = mcrfpy.Scene("demo") +mcrfpy.current_scene = scene + +texts = [ + "Short", + "A medium length text", + "This is a much longer piece of text to display", +] + +y = 150 +for text in texts: + # Create caption + cap = mcrfpy.Caption(text=text, pos=(100, y)) + cap.fill_color = mcrfpy.Color(255, 255, 255) + scene.children.append(cap) + + # Bounding box showing dimensions + box = mcrfpy.Frame( + pos=(100, y - 5), + size=(cap.w, cap.h + 10), + fill_color=mcrfpy.Color(0, 0, 0, 0), + outline=1.0, + outline_color=mcrfpy.Color(255, 100, 100) + ) + scene.children.append(box) + + # Show dimensions + dims = mcrfpy.Caption(text=f"w={cap.w:.0f}, h={cap.h:.0f}", pos=(100, y + 50)) + dims.fill_color = mcrfpy.Color(150, 150, 150) + scene.children.append(dims) + + y += 150 diff --git a/tests/snippets/020_sprite_atlas.py b/tests/snippets/020_sprite_atlas.py new file mode 100644 index 0000000..c24fef4 --- /dev/null +++ b/tests/snippets/020_sprite_atlas.py @@ -0,0 +1,28 @@ +# mcrf: objects=[Caption,Color,Scene,Sprite] verified=0.2.8-dev@a7ba486 status=ok +# Sprite Atlas - Browse texture atlas +import mcrfpy + +scene = mcrfpy.Scene("demo") +mcrfpy.current_scene = scene + +# Display a grid of sprites from the default texture +title = mcrfpy.Caption(text="Default Texture Atlas (kenney_tinydungeon)", pos=(250, 30)) +title.fill_color = mcrfpy.Color(255, 220, 100) +scene.children.append(title) + +# Show sprites 0-99 in a 10x10 grid +for row in range(10): + for col in range(10): + idx = row * 10 + col + sprite = mcrfpy.Sprite( + texture=mcrfpy.default_texture, + sprite_index=idx, + pos=(112 + col * 80, 80 + row * 65), + scale=3.0 + ) + scene.children.append(sprite) + + # Index label + label = mcrfpy.Caption(text=str(idx), pos=(112 + col * 80 + 20, 80 + row * 65 + 48)) + label.fill_color = mcrfpy.Color(120, 120, 120) + scene.children.append(label) diff --git a/tests/snippets/021_sprite_scale.py b/tests/snippets/021_sprite_scale.py new file mode 100644 index 0000000..cf58af6 --- /dev/null +++ b/tests/snippets/021_sprite_scale.py @@ -0,0 +1,24 @@ +# mcrf: objects=[Caption,Color,Scene,Sprite] verified=0.2.8-dev@a7ba486 status=ok +# Sprite Scale - Resize sprites +import mcrfpy + +scene = mcrfpy.Scene("demo") +mcrfpy.current_scene = scene + +scales = [1.0, 2.0, 4.0, 8.0, 16.0] +for i, scale in enumerate(scales): + sprite = mcrfpy.Sprite( + texture=mcrfpy.default_texture, + sprite_index=84, # Knight + pos=(100 + i * 180, 300), + scale=scale + ) + scene.children.append(sprite) + + label = mcrfpy.Caption(text=f"scale={scale}", pos=(100 + i * 180, 550)) + label.fill_color = mcrfpy.Color(200, 200, 200) + scene.children.append(label) + +title = mcrfpy.Caption(text="Sprite Scaling", pos=(420, 100)) +title.fill_color = mcrfpy.Color(255, 220, 100) +scene.children.append(title) diff --git a/tests/snippets/022_sprite_tint.py b/tests/snippets/022_sprite_tint.py new file mode 100644 index 0000000..61d8a2b --- /dev/null +++ b/tests/snippets/022_sprite_tint.py @@ -0,0 +1,32 @@ +# mcrf: objects=[Caption,Color,Scene,Sprite] verified=0.2.8-dev@a7ba486 status=ok +# Sprite Tint - Note: Sprite tinting requires shader +import mcrfpy + +scene = mcrfpy.Scene("demo") +mcrfpy.current_scene = scene + +# Note: Direct sprite color tinting is not available +# Use opacity or shaders for color effects + +opacities = [1.0, 0.8, 0.6, 0.4, 0.2] +labels = ["100%", "80%", "60%", "40%", "20%"] + +for i, (opacity, label_text) in enumerate(zip(opacities, labels)): + x = 100 + (i % 3) * 300 + y = 200 + (i // 3) * 280 + + sprite = mcrfpy.Sprite( + texture=mcrfpy.default_texture, + sprite_index=84, + pos=(x, y), + scale=8.0 + ) + sprite.opacity = opacity + scene.children.append(sprite) + + label = mcrfpy.Caption(text=f"opacity={label_text}", pos=(x, y + 150)) + scene.children.append(label) + +title = mcrfpy.Caption(text="Sprite Opacity Levels", pos=(380, 80)) +title.fill_color = mcrfpy.Color(255, 220, 100) +scene.children.append(title) diff --git a/tests/snippets/023_grid_tiles.py b/tests/snippets/023_grid_tiles.py new file mode 100644 index 0000000..c64f048 --- /dev/null +++ b/tests/snippets/023_grid_tiles.py @@ -0,0 +1,39 @@ +# mcrf: objects=[Grid,Scene] verified=0.2.8-dev@a7ba486 status=ok +# Grid Tiles - Different tile types +import mcrfpy + +scene = mcrfpy.Scene("demo") +mcrfpy.current_scene = scene + +grid = mcrfpy.Grid( + grid_size=(16, 12), + texture=mcrfpy.default_texture, + pos=(0, 0), + size=(1024, 768), + zoom=4.0 +) +scene.children.append(grid) + +# Different terrain types +tile_types = [ + (48, "Floor"), # Floor + (1, "Wall"), # Wall + (65, "Door"), # Door + (17, "Chest"), # Chest + (80, "Water"), # Water +] + +# Fill grid with pattern showing different tiles +for y in range(12): + for x in range(16): + cell = grid.at(x, y) + # Border walls + if x == 0 or x == 15 or y == 0 or y == 11: + cell.tilesprite = 1 + else: + # Show different tiles in sections + section = (x - 1) // 3 + if section < len(tile_types): + cell.tilesprite = tile_types[section][0] + else: + cell.tilesprite = 48 diff --git a/tests/snippets/024_grid_walkable.py b/tests/snippets/024_grid_walkable.py new file mode 100644 index 0000000..4bb6f99 --- /dev/null +++ b/tests/snippets/024_grid_walkable.py @@ -0,0 +1,37 @@ +# mcrf: objects=[Caption,Color,Grid,Scene] verified=0.2.8-dev@a7ba486 status=ok +# Grid Walkable - Walkable vs blocked tiles +import mcrfpy + +scene = mcrfpy.Scene("demo") +mcrfpy.current_scene = scene + +grid = mcrfpy.Grid( + grid_size=(16, 12), + texture=mcrfpy.default_texture, + pos=(0, 0), + size=(1024, 768), + zoom=4.0 +) +scene.children.append(grid) + +# Create maze-like pattern +for y in range(12): + for x in range(16): + cell = grid.at(x, y) + # Outer walls + if x == 0 or x == 15 or y == 0 or y == 11: + cell.tilesprite = 1 # Wall + cell.walkable = False + # Inner obstacles + elif (x + y) % 4 == 0 and x > 2 and x < 13: + cell.tilesprite = 1 + cell.walkable = False + else: + cell.tilesprite = 48 # Floor + cell.walkable = True + +# Legend +legend = mcrfpy.Caption(text="Walls are not walkable, floors are", pos=(350, 720)) +legend.outline = 2 +legend.outline_color = mcrfpy.Color(0, 0, 0) +scene.children.append(legend) diff --git a/tests/snippets/025_grid_cell_color.py b/tests/snippets/025_grid_cell_color.py new file mode 100644 index 0000000..50bb79f --- /dev/null +++ b/tests/snippets/025_grid_cell_color.py @@ -0,0 +1,39 @@ +# mcrf: objects=[Caption,Color,ColorLayer,Grid,Scene] verified=0.2.8-dev@a7ba486 status=ok +# Grid Cell Colors - Use color layer for cell colors +import mcrfpy + +scene = mcrfpy.Scene("demo") +mcrfpy.current_scene = scene + +grid = mcrfpy.Grid( + grid_size=(16, 12), + texture=mcrfpy.default_texture, + pos=(0, 0), + size=(1024, 768), + zoom=4.0 +) +scene.children.append(grid) + +# Set tiles +for y in range(12): + for x in range(16): + grid.at(x, y).tilesprite = 48 + +# Add a color layer for tinting +color_layer = mcrfpy.ColorLayer(z_index=1, name="tint") +grid.add_layer(color_layer) + +# Create colorful gradient pattern using color layer +for y in range(12): + for x in range(16): + # Rainbow gradient based on position + r = int(255 * x / 16) + g = int(255 * y / 12) + b = int(255 * (16 - x) / 16) + color_layer.set((x, y), mcrfpy.Color(r, g, b, 180)) + +title = mcrfpy.Caption(text="Per-Cell Color Tinting via Layer", pos=(350, 720)) +title.fill_color = mcrfpy.Color(255, 220, 100) +title.outline = 2 +title.outline_color = mcrfpy.Color(0, 0, 0) +scene.children.append(title) diff --git a/tests/snippets/026_grid_camera_center.py b/tests/snippets/026_grid_camera_center.py new file mode 100644 index 0000000..b4ab36a --- /dev/null +++ b/tests/snippets/026_grid_camera_center.py @@ -0,0 +1,60 @@ +# mcrf: objects=[Caption,Color,Entity,Grid,InputState,Key,Scene] verified=0.2.8-dev@a7ba486 status=ok +# Grid Camera Center - Pan the view +import mcrfpy + +scene = mcrfpy.Scene("demo") +mcrfpy.current_scene = scene + +# Large grid, smaller viewport +grid = mcrfpy.Grid( + grid_size=(30, 30), + texture=mcrfpy.default_texture, + pos=(212, 134), + size=(600, 500) +) +scene.children.append(grid) + +# Fill with pattern to show scrolling +for y in range(30): + for x in range(30): + cell = grid.at(x, y) + if (x + y) % 2 == 0: + cell.tilesprite = 48 + else: + cell.tilesprite = 49 + +# Center camera on specific tile (15, 15) +grid.center_camera((15, 15)) + +# Add marker at center +marker = mcrfpy.Entity( + grid_pos=(15, 15), + texture=mcrfpy.default_texture, + sprite_index=84 +) +grid.entities.append(marker) + +status = mcrfpy.Caption(text="WASD to pan camera - centered on (15, 15)", pos=(300, 660)) +status.outline = 2 +status.outline_color = mcrfpy.Color(0, 0, 0) +scene.children.append(status) + +# Track camera position +camera_pos = [15, 15] + +def on_key(key, action): + if action != mcrfpy.InputState.PRESSED: + return + dx, dy = 0, 0 + if key == mcrfpy.Key.W: dy = -1 + elif key == mcrfpy.Key.S: dy = 1 + elif key == mcrfpy.Key.A: dx = -1 + elif key == mcrfpy.Key.D: dx = 1 + else: return + + camera_pos[0] = max(0, min(29, camera_pos[0] + dx)) + camera_pos[1] = max(0, min(29, camera_pos[1] + dy)) + grid.center_camera((camera_pos[0], camera_pos[1])) + status.text = f"WASD to pan camera - centered on ({camera_pos[0]}, {camera_pos[1]})" + +scene.on_key = on_key diff --git a/tests/snippets/027_grid_zoom.py b/tests/snippets/027_grid_zoom.py new file mode 100644 index 0000000..d5404d7 --- /dev/null +++ b/tests/snippets/027_grid_zoom.py @@ -0,0 +1,41 @@ +# mcrf: objects=[Caption,Color,Entity,Frame,Grid,Scene] verified=0.2.8-dev@a7ba486 status=ok +# Grid Zoom - Scale the grid view +import mcrfpy + +scene = mcrfpy.Scene("demo") +mcrfpy.current_scene = scene + +zooms = [0.5, 1.0, 1.5, 2.0] +for i, zoom in enumerate(zooms): + x = 62 + (i % 2) * 480 + y = 84 + (i // 2) * 340 + + grid = mcrfpy.Grid( + grid_size=(10, 8), + texture=mcrfpy.default_texture, + pos=(x, y), + size=(420, 280) + ) + scene.children.append(grid) + + # Fill with pattern + for gy in range(8): + for gx in range(10): + cell = grid.at(gx, gy) + cell.tilesprite = 48 if (gx + gy) % 2 == 0 else 49 + + # Add entity + ent = mcrfpy.Entity(grid_pos=(5, 4), texture=mcrfpy.default_texture, sprite_index=84) + grid.entities.append(ent) + + # Apply zoom + grid.zoom = zoom + grid.center_camera((5, 4)) + + label = mcrfpy.Caption(text=f"zoom={zoom}", pos=(x + 170, y + 290)) + label.outline = 2 + label.outline_color = mcrfpy.Color(0, 0, 0) + scene.children.append(label) + +# Add background and title +scene.children.insert(0, mcrfpy.Frame(pos=(0, 0), size=(1024, 768), fill_color=mcrfpy.Color(30, 30, 40))) diff --git a/tests/snippets/028_entity_movement.py b/tests/snippets/028_entity_movement.py new file mode 100644 index 0000000..b3a09ba --- /dev/null +++ b/tests/snippets/028_entity_movement.py @@ -0,0 +1,49 @@ +# mcrf: objects=[Caption,Color,Entity,Grid,InputState,Key,Scene] verified=0.2.8-dev@a7ba486 status=ok +# Entity Movement - Move entities on grid +import mcrfpy + +scene = mcrfpy.Scene("demo") +mcrfpy.current_scene = scene + +grid = mcrfpy.Grid( + grid_size=(16, 12), + texture=mcrfpy.default_texture, + pos=(0, 0), + size=(1024, 768), + zoom=4.0 +) +scene.children.append(grid) + +# Floor tiles +for y in range(12): + for x in range(16): + grid.at(x, y).tilesprite = 48 + +# Create player entity +player = mcrfpy.Entity( + grid_pos=(8, 6), + texture=mcrfpy.default_texture, + sprite_index=84 +) +grid.entities.append(player) + +# Movement with keyboard +def on_key(key, action): + if action != mcrfpy.InputState.PRESSED: + return + x, y = player.grid_x, player.grid_y + if key == mcrfpy.Key.UP or key == mcrfpy.Key.W: + player.grid_pos = (x, y - 1) + elif key == mcrfpy.Key.DOWN or key == mcrfpy.Key.S: + player.grid_pos = (x, y + 1) + elif key == mcrfpy.Key.LEFT or key == mcrfpy.Key.A: + player.grid_pos = (x - 1, y) + elif key == mcrfpy.Key.RIGHT or key == mcrfpy.Key.D: + player.grid_pos = (x + 1, y) + +scene.on_key = on_key + +status = mcrfpy.Caption(text="WASD or Arrow keys to move", pos=(380, 720)) +status.outline = 2 +status.outline_color = mcrfpy.Color(0, 0, 0) +scene.children.append(status) diff --git a/tests/snippets/029_entity_multiple.py b/tests/snippets/029_entity_multiple.py new file mode 100644 index 0000000..8106bed --- /dev/null +++ b/tests/snippets/029_entity_multiple.py @@ -0,0 +1,44 @@ +# mcrf: objects=[Caption,Color,Entity,Grid,Scene] verified=0.2.8-dev@a7ba486 status=ok +# Multiple Entities - Several entities on one grid +import mcrfpy + +scene = mcrfpy.Scene("demo") +mcrfpy.current_scene = scene + +grid = mcrfpy.Grid( + grid_size=(16, 12), + texture=mcrfpy.default_texture, + pos=(0, 0), + size=(1024, 768), + zoom=4.0 +) +scene.children.append(grid) + +# Floor tiles +for y in range(12): + for x in range(16): + grid.at(x, y).tilesprite = 48 + +# Different entity types +entities = [ + (4, 3, 84, "Knight"), # Knight + (8, 3, 86, "Mage"), # Mage + (12, 3, 88, "Archer"), # Archer + (4, 8, 112, "Goblin"), # Goblin + (8, 8, 116, "Skeleton"),# Skeleton + (12, 8, 120, "Slime"), # Slime +] + +for x, y, sprite_idx, name in entities: + entity = mcrfpy.Entity( + grid_pos=(x, y), + texture=mcrfpy.default_texture, + sprite_index=sprite_idx, + name=name + ) + grid.entities.append(entity) + +status = mcrfpy.Caption(text=f"Entity count: {len(grid.entities)}", pos=(420, 720)) +status.outline = 2 +status.outline_color = mcrfpy.Color(0, 0, 0) +scene.children.append(status) diff --git a/tests/snippets/030_entity_draw_pos.py b/tests/snippets/030_entity_draw_pos.py new file mode 100644 index 0000000..7d0b43d --- /dev/null +++ b/tests/snippets/030_entity_draw_pos.py @@ -0,0 +1,48 @@ +# mcrf: objects=[Caption,Color,Entity,Grid,Scene,Timer,Vector] verified=0.2.8-dev@a7ba486 status=ok +# Entity Draw Position - Smooth sub-tile positioning +import mcrfpy + +scene = mcrfpy.Scene("demo") +mcrfpy.current_scene = scene + +grid = mcrfpy.Grid( + grid_size=(16, 12), + texture=mcrfpy.default_texture, + pos=(0, 0), + size=(1024, 768), + zoom=4.0 +) +scene.children.append(grid) + +# Floor tiles +for y in range(12): + for x in range(16): + grid.at(x, y).tilesprite = 48 + +# Entity at grid position with draw offset +entity = mcrfpy.Entity( + grid_pos=(8, 6), + texture=mcrfpy.default_texture, + sprite_index=84 +) +grid.entities.append(entity) + +# Animate the draw position for smooth movement +offset = 0.0 +direction = 1 + +def animate_offset(timer, runtime): + global offset, direction + offset += 0.02 * direction + if offset > 0.5: + direction = -1 + elif offset < -0.5: + direction = 1 + entity.draw_pos = mcrfpy.Vector(8 + offset, 6) + +timer = mcrfpy.Timer("move", animate_offset, 16) + +status = mcrfpy.Caption(text="draw_pos allows sub-tile positioning", pos=(340, 720)) +status.outline = 2 +status.outline_color = mcrfpy.Color(0, 0, 0) +scene.children.append(status) diff --git a/tests/snippets/031_animation_basic.py b/tests/snippets/031_animation_basic.py new file mode 100644 index 0000000..fcb83eb --- /dev/null +++ b/tests/snippets/031_animation_basic.py @@ -0,0 +1,38 @@ +# mcrf: objects=[Caption,Color,Easing,Frame,InputState,Key,Scene,Timer] verified=0.2.8-dev@a7ba486 status=ok +# Animation Basic - Animate position +import mcrfpy + +scene = mcrfpy.Scene("demo") +mcrfpy.current_scene = scene + +# Dark background +scene.children.append(mcrfpy.Frame(pos=(0, 0), size=(1024, 768), fill_color=mcrfpy.Color(30, 30, 40))) + +# Create a frame to animate +frame = mcrfpy.Frame( + pos=(100, 334), + size=(150, 100), + fill_color=mcrfpy.Color(100, 150, 255) +) +scene.children.append(frame) + +label = mcrfpy.Caption(text="SPACE to restart animation", pos=(380, 700)) +label.outline = 2 +label.outline_color = mcrfpy.Color(0, 0, 0) +scene.children.append(label) + +# Animate position when space pressed +def on_key(key, action): + if key == mcrfpy.Key.SPACE and action == mcrfpy.InputState.PRESSED: + frame.x = 100 + frame.animate("x", 774, 2.0, mcrfpy.Easing.LINEAR) + +scene.on_key = on_key + +def restart_animation(timer, runtime): + frame.x = 100 + frame.animate("x", 774, 2.0, mcrfpy.Easing.LINEAR) + +# Auto-start animation with looping +frame.animate("x", 774, 2.0, mcrfpy.Easing.LINEAR) +loop_timer = mcrfpy.Timer("loop", restart_animation, 7000) diff --git a/tests/snippets/032_animation_easing_linear.py b/tests/snippets/032_animation_easing_linear.py new file mode 100644 index 0000000..d9a770c --- /dev/null +++ b/tests/snippets/032_animation_easing_linear.py @@ -0,0 +1,42 @@ +# mcrf: objects=[Caption,Color,Easing,Frame,Scene,Timer] verified=0.2.8-dev@a7ba486 status=ok +# Easing: LINEAR - Constant speed +import mcrfpy + +scene = mcrfpy.Scene("demo") +mcrfpy.current_scene = scene + +# Background +scene.children.append(mcrfpy.Frame( + pos=(0, 0), size=(1024, 768), + fill_color=mcrfpy.Color(30, 30, 40) +)) + +# Moving frame +frame = mcrfpy.Frame( + pos=(100, 334), size=(100, 100), + fill_color=mcrfpy.Color(255, 150, 100) +) +scene.children.append(frame) + +# Start/end markers +scene.children.append(mcrfpy.Frame( + pos=(100, 300), size=(5, 170), + fill_color=mcrfpy.Color(100, 100, 100) +)) +scene.children.append(mcrfpy.Frame( + pos=(824, 300), size=(5, 170), + fill_color=mcrfpy.Color(100, 100, 100) +)) + +_caption = mcrfpy.Caption( + text="LINEAR - Constant velocity", + pos=(380, 200)) +_caption.fill_color = mcrfpy.Color(255, 220, 100) +scene.children.append(_caption) + +def restart_animation(timer, runtime): + frame.x = 100 + frame.animate("x", 824, 3.0, mcrfpy.Easing.LINEAR) + +frame.animate("x", 824, 3.0, mcrfpy.Easing.LINEAR) +loop_timer = mcrfpy.Timer("loop", restart_animation, 8000) # 3s animation + 5s pause diff --git a/tests/snippets/033_animation_easing_ease_in.py b/tests/snippets/033_animation_easing_ease_in.py new file mode 100644 index 0000000..263b9d9 --- /dev/null +++ b/tests/snippets/033_animation_easing_ease_in.py @@ -0,0 +1,34 @@ +# mcrf: objects=[Caption,Color,Easing,Frame,Scene,Timer] verified=0.2.8-dev@a7ba486 status=ok +# Easing: EASE_IN - Slow start, fast end +import mcrfpy + +scene = mcrfpy.Scene("demo") +mcrfpy.current_scene = scene + +scene.children.append(mcrfpy.Frame( + pos=(0, 0), size=(1024, 768), + fill_color=mcrfpy.Color(30, 30, 40) +)) + +frame = mcrfpy.Frame( + pos=(100, 334), size=(100, 100), + fill_color=mcrfpy.Color(100, 255, 150) +) +scene.children.append(frame) + +# Markers +scene.children.append(mcrfpy.Frame(pos=(100, 300), size=(5, 170), fill_color=mcrfpy.Color(100, 100, 100))) +scene.children.append(mcrfpy.Frame(pos=(824, 300), size=(5, 170), fill_color=mcrfpy.Color(100, 100, 100))) + +_caption = mcrfpy.Caption( + text="EASE_IN - Accelerates from rest", + pos=(350, 200)) +_caption.fill_color = mcrfpy.Color(255, 220, 100) +scene.children.append(_caption) + +def restart_animation(timer, runtime): + frame.x = 100 + frame.animate("x", 824, 3.0, mcrfpy.Easing.EASE_IN) + +frame.animate("x", 824, 3.0, mcrfpy.Easing.EASE_IN) +loop_timer = mcrfpy.Timer("loop", restart_animation, 8000) diff --git a/tests/snippets/034_animation_easing_ease_out.py b/tests/snippets/034_animation_easing_ease_out.py new file mode 100644 index 0000000..3514aec --- /dev/null +++ b/tests/snippets/034_animation_easing_ease_out.py @@ -0,0 +1,34 @@ +# mcrf: objects=[Caption,Color,Easing,Frame,Scene,Timer] verified=0.2.8-dev@a7ba486 status=ok +# Easing: EASE_OUT - Fast start, slow end +import mcrfpy + +scene = mcrfpy.Scene("demo") +mcrfpy.current_scene = scene + +scene.children.append(mcrfpy.Frame( + pos=(0, 0), size=(1024, 768), + fill_color=mcrfpy.Color(30, 30, 40) +)) + +frame = mcrfpy.Frame( + pos=(100, 334), size=(100, 100), + fill_color=mcrfpy.Color(150, 100, 255) +) +scene.children.append(frame) + +# Markers +scene.children.append(mcrfpy.Frame(pos=(100, 300), size=(5, 170), fill_color=mcrfpy.Color(100, 100, 100))) +scene.children.append(mcrfpy.Frame(pos=(824, 300), size=(5, 170), fill_color=mcrfpy.Color(100, 100, 100))) + +_caption = mcrfpy.Caption( + text="EASE_OUT - Decelerates to rest", + pos=(350, 200)) +_caption.fill_color = mcrfpy.Color(255, 220, 100) +scene.children.append(_caption) + +def restart_animation(timer, runtime): + frame.x = 100 + frame.animate("x", 824, 3.0, mcrfpy.Easing.EASE_OUT) + +frame.animate("x", 824, 3.0, mcrfpy.Easing.EASE_OUT) +loop_timer = mcrfpy.Timer("loop", restart_animation, 8000) diff --git a/tests/snippets/035_animation_easing_ease_in_out.py b/tests/snippets/035_animation_easing_ease_in_out.py new file mode 100644 index 0000000..e808c8e --- /dev/null +++ b/tests/snippets/035_animation_easing_ease_in_out.py @@ -0,0 +1,34 @@ +# mcrf: objects=[Caption,Color,Easing,Frame,Scene,Timer] verified=0.2.8-dev@a7ba486 status=ok +# Easing: EASE_IN_OUT - Smooth both ends +import mcrfpy + +scene = mcrfpy.Scene("demo") +mcrfpy.current_scene = scene + +scene.children.append(mcrfpy.Frame( + pos=(0, 0), size=(1024, 768), + fill_color=mcrfpy.Color(30, 30, 40) +)) + +frame = mcrfpy.Frame( + pos=(100, 334), size=(100, 100), + fill_color=mcrfpy.Color(255, 200, 100) +) +scene.children.append(frame) + +# Markers +scene.children.append(mcrfpy.Frame(pos=(100, 300), size=(5, 170), fill_color=mcrfpy.Color(100, 100, 100))) +scene.children.append(mcrfpy.Frame(pos=(824, 300), size=(5, 170), fill_color=mcrfpy.Color(100, 100, 100))) + +_caption = mcrfpy.Caption( + text="EASE_IN_OUT - Smooth acceleration & deceleration", + pos=(300, 200)) +_caption.fill_color = mcrfpy.Color(255, 220, 100) +scene.children.append(_caption) + +def restart_animation(timer, runtime): + frame.x = 100 + frame.animate("x", 824, 3.0, mcrfpy.Easing.EASE_IN_OUT) + +frame.animate("x", 824, 3.0, mcrfpy.Easing.EASE_IN_OUT) +loop_timer = mcrfpy.Timer("loop", restart_animation, 8000) diff --git a/tests/snippets/036_animation_easing_bounce.py b/tests/snippets/036_animation_easing_bounce.py new file mode 100644 index 0000000..563982c --- /dev/null +++ b/tests/snippets/036_animation_easing_bounce.py @@ -0,0 +1,37 @@ +# mcrf: objects=[Caption,Color,Easing,Frame,Scene,Timer] verified=0.2.8-dev@a7ba486 status=ok +# Easing: BOUNCE - Bouncy effect +import mcrfpy + +scene = mcrfpy.Scene("demo") +mcrfpy.current_scene = scene + +scene.children.append(mcrfpy.Frame( + pos=(0, 0), size=(1024, 768), + fill_color=mcrfpy.Color(30, 30, 40) +)) + +# Ball that bounces +ball = mcrfpy.Frame( + pos=(462, 100), size=(100, 100), + fill_color=mcrfpy.Color(255, 100, 100) +) +scene.children.append(ball) + +# Ground line +scene.children.append(mcrfpy.Frame( + pos=(0, 600), size=(1024, 5), + fill_color=mcrfpy.Color(100, 100, 100) +)) + +_caption = mcrfpy.Caption( + text="EASE_OUT_BOUNCE - Ball drop effect", + pos=(340, 680)) +_caption.fill_color = mcrfpy.Color(255, 220, 100) +scene.children.append(_caption) + +def restart_animation(timer, runtime): + ball.y = 100 + ball.animate("y", 500, 2.0, mcrfpy.Easing.EASE_OUT_BOUNCE) + +ball.animate("y", 500, 2.0, mcrfpy.Easing.EASE_OUT_BOUNCE) +loop_timer = mcrfpy.Timer("loop", restart_animation, 7000) # 2s animation + 5s pause diff --git a/tests/snippets/037_animation_easing_elastic.py b/tests/snippets/037_animation_easing_elastic.py new file mode 100644 index 0000000..9412b26 --- /dev/null +++ b/tests/snippets/037_animation_easing_elastic.py @@ -0,0 +1,36 @@ +# mcrf: objects=[Caption,Color,Easing,Frame,Scene,Timer] verified=0.2.8-dev@a7ba486 status=ok +# Easing: ELASTIC - Springy overshoot +import mcrfpy + +scene = mcrfpy.Scene("demo") +mcrfpy.current_scene = scene + +scene.children.append(mcrfpy.Frame( + pos=(0, 0), size=(1024, 768), + fill_color=mcrfpy.Color(30, 30, 40) +)) + +frame = mcrfpy.Frame( + pos=(100, 334), size=(100, 100), + fill_color=mcrfpy.Color(100, 200, 255) +) +scene.children.append(frame) + +# Target marker +scene.children.append(mcrfpy.Frame( + pos=(824, 300), size=(5, 170), + fill_color=mcrfpy.Color(255, 100, 100) +)) + +_caption = mcrfpy.Caption( + text="EASE_OUT_ELASTIC - Spring overshoot", + pos=(340, 200)) +_caption.fill_color = mcrfpy.Color(255, 220, 100) +scene.children.append(_caption) + +def restart_animation(timer, runtime): + frame.x = 100 + frame.animate("x", 824, 2.5, mcrfpy.Easing.EASE_OUT_ELASTIC) + +frame.animate("x", 824, 2.5, mcrfpy.Easing.EASE_OUT_ELASTIC) +loop_timer = mcrfpy.Timer("loop", restart_animation, 7500) # 2.5s animation + 5s pause diff --git a/tests/snippets/038_animation_easing_back.py b/tests/snippets/038_animation_easing_back.py new file mode 100644 index 0000000..db633ad --- /dev/null +++ b/tests/snippets/038_animation_easing_back.py @@ -0,0 +1,34 @@ +# mcrf: objects=[Caption,Color,Easing,Frame,Scene,Timer] verified=0.2.8-dev@a7ba486 status=ok +# Easing: BACK - Anticipation/overshoot +import mcrfpy + +scene = mcrfpy.Scene("demo") +mcrfpy.current_scene = scene + +scene.children.append(mcrfpy.Frame( + pos=(0, 0), size=(1024, 768), + fill_color=mcrfpy.Color(30, 30, 40) +)) + +frame = mcrfpy.Frame( + pos=(100, 334), size=(100, 100), + fill_color=mcrfpy.Color(200, 150, 255) +) +scene.children.append(frame) + +# Markers +scene.children.append(mcrfpy.Frame(pos=(100, 300), size=(5, 170), fill_color=mcrfpy.Color(100, 100, 100))) +scene.children.append(mcrfpy.Frame(pos=(824, 300), size=(5, 170), fill_color=mcrfpy.Color(100, 100, 100))) + +_caption = mcrfpy.Caption( + text="EASE_IN_OUT_BACK - Pull back, then overshoot", + pos=(300, 200)) +_caption.fill_color = mcrfpy.Color(255, 220, 100) +scene.children.append(_caption) + +def restart_animation(timer, runtime): + frame.x = 100 + frame.animate("x", 824, 2.0, mcrfpy.Easing.EASE_IN_OUT_BACK) + +frame.animate("x", 824, 2.0, mcrfpy.Easing.EASE_IN_OUT_BACK) +loop_timer = mcrfpy.Timer("loop", restart_animation, 7000) # 2s animation + 5s pause diff --git a/tests/snippets/039_animation_easing_comparison.py b/tests/snippets/039_animation_easing_comparison.py new file mode 100644 index 0000000..ce7ba01 --- /dev/null +++ b/tests/snippets/039_animation_easing_comparison.py @@ -0,0 +1,43 @@ +# mcrf: objects=[Caption,Color,Easing,Frame,Scene,Timer] verified=0.2.8-dev@a7ba486 status=ok +# Easing Comparison - Multiple easings side by side +import mcrfpy + +scene = mcrfpy.Scene("demo") +mcrfpy.current_scene = scene + +scene.children.append(mcrfpy.Frame( + pos=(0, 0), size=(1024, 768), + fill_color=mcrfpy.Color(30, 30, 40) +)) + +easings = [ + (mcrfpy.Easing.LINEAR, "LINEAR", mcrfpy.Color(255, 100, 100)), + (mcrfpy.Easing.EASE_IN_QUAD, "EASE_IN_QUAD", mcrfpy.Color(255, 200, 100)), + (mcrfpy.Easing.EASE_OUT_QUAD, "EASE_OUT_QUAD", mcrfpy.Color(200, 255, 100)), + (mcrfpy.Easing.EASE_IN_OUT_QUAD, "EASE_IN_OUT_QUAD", mcrfpy.Color(100, 255, 100)), + (mcrfpy.Easing.EASE_OUT_BOUNCE, "EASE_OUT_BOUNCE", mcrfpy.Color(100, 255, 200)), + (mcrfpy.Easing.EASE_OUT_ELASTIC, "EASE_OUT_ELASTIC", mcrfpy.Color(100, 200, 255)), +] + +frames = [] +for i, (easing, name, color) in enumerate(easings): + y = 80 + i * 110 + + label = mcrfpy.Caption(text=name, pos=(100, y + 15)) + label.fill_color = mcrfpy.Color(180, 180, 180) + scene.children.append(label) + + frame = mcrfpy.Frame( + pos=(350, y), size=(60, 50), + fill_color=color + ) + scene.children.append(frame) + frames.append((frame, easing)) + frame.animate("x", 900, 3.0, easing) + +def restart_animation(timer, runtime): + for frame, easing in frames: + frame.x = 350 + frame.animate("x", 900, 3.0, easing) + +loop_timer = mcrfpy.Timer("loop", restart_animation, 8000) diff --git a/tests/snippets/040_animation_color.py b/tests/snippets/040_animation_color.py new file mode 100644 index 0000000..2fdcb92 --- /dev/null +++ b/tests/snippets/040_animation_color.py @@ -0,0 +1,34 @@ +# mcrf: objects=[Caption,Color,Easing,Frame,Scene,Timer] verified=0.2.8-dev@a7ba486 status=ok +# Animation Color - Animate fill_color +import mcrfpy + +scene = mcrfpy.Scene("demo") +mcrfpy.current_scene = scene + +scene.children.append(mcrfpy.Frame( + pos=(0, 0), size=(1024, 768), + fill_color=mcrfpy.Color(30, 30, 40) +)) + +# Large frame to show color change +frame = mcrfpy.Frame( + pos=(262, 184), size=(500, 400), + fill_color=mcrfpy.Color(255, 100, 100), + outline=3.0, + outline_color=mcrfpy.Color(255, 255, 255) +) +scene.children.append(frame) + +_caption = mcrfpy.Caption( + text="Animating fill_color from red to blue", + pos=(340, 620)) +_caption.fill_color = mcrfpy.Color(255, 220, 100) +scene.children.append(_caption) + +def restart_animation(timer, runtime): + frame.fill_color = mcrfpy.Color(255, 100, 100) + frame.animate("fill_color", (100, 100, 255, 255), 3.0, mcrfpy.Easing.EASE_IN_OUT) + +# Animate to blue (pass color as RGBA tuple) +frame.animate("fill_color", (100, 100, 255, 255), 3.0, mcrfpy.Easing.EASE_IN_OUT) +loop_timer = mcrfpy.Timer("loop", restart_animation, 8000) diff --git a/tests/snippets/041_animation_opacity.py b/tests/snippets/041_animation_opacity.py new file mode 100644 index 0000000..33b15d9 --- /dev/null +++ b/tests/snippets/041_animation_opacity.py @@ -0,0 +1,38 @@ +# mcrf: objects=[Caption,Color,Easing,Frame,Scene,Timer] verified=0.2.8-dev@a7ba486 status=ok +# Animation Opacity - Fade in/out +import mcrfpy + +scene = mcrfpy.Scene("demo") +mcrfpy.current_scene = scene + +scene.children.append(mcrfpy.Frame( + pos=(0, 0), size=(1024, 768), + fill_color=mcrfpy.Color(30, 30, 40) +)) + +# Frame that fades out +frame = mcrfpy.Frame( + pos=(262, 184), size=(500, 400), + fill_color=mcrfpy.Color(200, 100, 255), + opacity=1.0 +) +scene.children.append(frame) + +label = mcrfpy.Caption( + text="I'm fading away...", + pos=(170, 180)) +label.fill_color = mcrfpy.Color(255, 255, 255) +frame.children.append(label) + +_caption = mcrfpy.Caption( + text="Animating opacity from 1.0 to 0.0", + pos=(360, 620)) +_caption.fill_color = mcrfpy.Color(255, 220, 100) +scene.children.append(_caption) + +def restart_animation(timer, runtime): + frame.opacity = 1.0 + frame.animate("opacity", 0.0, 4.0, mcrfpy.Easing.EASE_IN) + +frame.animate("opacity", 0.0, 4.0, mcrfpy.Easing.EASE_IN) +loop_timer = mcrfpy.Timer("loop", restart_animation, 9000) # 4s animation + 5s pause diff --git a/tests/snippets/042_animation_size.py b/tests/snippets/042_animation_size.py new file mode 100644 index 0000000..304a469 --- /dev/null +++ b/tests/snippets/042_animation_size.py @@ -0,0 +1,37 @@ +# mcrf: objects=[Caption,Color,Easing,Frame,Scene,Timer] verified=0.2.8-dev@a7ba486 status=ok +# Animation Size - Grow and shrink +import mcrfpy + +scene = mcrfpy.Scene("demo") +mcrfpy.current_scene = scene + +scene.children.append(mcrfpy.Frame( + pos=(0, 0), size=(1024, 768), + fill_color=mcrfpy.Color(30, 30, 40) +)) + +# Frame that grows +frame = mcrfpy.Frame( + pos=(462, 334), size=(100, 100), + fill_color=mcrfpy.Color(100, 255, 150), + outline=2.0, + outline_color=mcrfpy.Color(255, 255, 255) +) +scene.children.append(frame) + +_caption = mcrfpy.Caption( + text="Animating w and h properties", + pos=(380, 620)) +_caption.fill_color = mcrfpy.Color(255, 220, 100) +scene.children.append(_caption) + +def restart_animation(timer, runtime): + frame.w = 100 + frame.h = 100 + frame.animate("w", 400, 2.0, mcrfpy.Easing.EASE_OUT_ELASTIC) + frame.animate("h", 300, 2.0, mcrfpy.Easing.EASE_OUT_ELASTIC) + +# Animate both dimensions +frame.animate("w", 400, 2.0, mcrfpy.Easing.EASE_OUT_ELASTIC) +frame.animate("h", 300, 2.0, mcrfpy.Easing.EASE_OUT_ELASTIC) +loop_timer = mcrfpy.Timer("loop", restart_animation, 7000) # 2s animation + 5s pause diff --git a/tests/snippets/043_animation_sprite_index.py b/tests/snippets/043_animation_sprite_index.py new file mode 100644 index 0000000..73bb596 --- /dev/null +++ b/tests/snippets/043_animation_sprite_index.py @@ -0,0 +1,34 @@ +# mcrf: objects=[Caption,Color,Easing,Frame,Scene,Sprite,Timer] verified=0.2.8-dev@a7ba486 status=ok +# Animation Sprite Index - Sprite animation +import mcrfpy + +scene = mcrfpy.Scene("demo") +mcrfpy.current_scene = scene + +scene.children.append(mcrfpy.Frame( + pos=(0, 0), size=(1024, 768), + fill_color=mcrfpy.Color(30, 30, 40) +)) + +# Large sprite for visibility +sprite = mcrfpy.Sprite( + texture=mcrfpy.default_texture, + sprite_index=84, + pos=(400, 250), + scale=12.0 +) +scene.children.append(sprite) + +_caption = mcrfpy.Caption( + text="Animating sprite_index (84 -> 120)", + pos=(360, 600)) +_caption.fill_color = mcrfpy.Color(255, 220, 100) +scene.children.append(_caption) + +def restart_animation(timer, runtime): + sprite.sprite_index = 84 + sprite.animate("sprite_index", 120, 3.0, mcrfpy.Easing.LINEAR) + +# Animate through sprite indices +sprite.animate("sprite_index", 120, 3.0, mcrfpy.Easing.LINEAR) +loop_timer = mcrfpy.Timer("loop", restart_animation, 8000) diff --git a/tests/snippets/044_animation_callback.py b/tests/snippets/044_animation_callback.py new file mode 100644 index 0000000..c001869 --- /dev/null +++ b/tests/snippets/044_animation_callback.py @@ -0,0 +1,37 @@ +# mcrf: objects=[Caption,Color,Easing,Frame,Scene,Timer] verified=0.2.8-dev@a7ba486 status=ok +# Animation Callback - Run code when animation ends +import mcrfpy + +scene = mcrfpy.Scene("demo") +mcrfpy.current_scene = scene + +scene.children.append(mcrfpy.Frame( + pos=(0, 0), size=(1024, 768), + fill_color=mcrfpy.Color(30, 30, 40) +)) + +frame = mcrfpy.Frame( + pos=(100, 334), size=(100, 100), + fill_color=mcrfpy.Color(255, 150, 100) +) +scene.children.append(frame) + +status = mcrfpy.Caption( + text="Animation in progress...", + pos=(380, 500)) +status.fill_color = mcrfpy.Color(200, 200, 200) +scene.children.append(status) + +# Callback when animation completes +def on_complete(target, prop, value): + status.text = f"Animation complete! {prop}={value:.0f}" + target.fill_color = mcrfpy.Color(100, 255, 100) + +def restart_animation(timer, runtime): + frame.x = 100 + frame.fill_color = mcrfpy.Color(255, 150, 100) + status.text = "Animation in progress..." + frame.animate("x", 824, 2.0, mcrfpy.Easing.EASE_OUT, callback=on_complete) + +frame.animate("x", 824, 2.0, mcrfpy.Easing.EASE_OUT, callback=on_complete) +loop_timer = mcrfpy.Timer("loop", restart_animation, 7000) # 2s animation + 5s pause diff --git a/tests/snippets/045_animation_chain.py b/tests/snippets/045_animation_chain.py new file mode 100644 index 0000000..2b3dabd --- /dev/null +++ b/tests/snippets/045_animation_chain.py @@ -0,0 +1,46 @@ +# mcrf: objects=[Caption,Color,Easing,Frame,Scene,Timer] verified=0.2.8-dev@a7ba486 status=ok +# Animation Chain - Sequential animations +import mcrfpy + +scene = mcrfpy.Scene("demo") +mcrfpy.current_scene = scene + +scene.children.append(mcrfpy.Frame( + pos=(0, 0), size=(1024, 768), + fill_color=mcrfpy.Color(30, 30, 40) +)) + +frame = mcrfpy.Frame( + pos=(100, 100), size=(100, 100), + fill_color=mcrfpy.Color(255, 100, 100) +) +scene.children.append(frame) + +status = mcrfpy.Caption(text="Step 1: Moving right", pos=(380, 620)) +scene.children.append(status) + +def step2(target, prop, value): + status.text = "Step 2: Moving down" + target.fill_color = mcrfpy.Color(100, 255, 100) + target.animate("y", 568, 1.0, mcrfpy.Easing.EASE_IN_OUT, callback=step3) + +def step3(target, prop, value): + status.text = "Step 3: Moving left" + target.fill_color = mcrfpy.Color(100, 100, 255) + target.animate("x", 100, 1.0, mcrfpy.Easing.EASE_IN_OUT, callback=step4) + +def step4(target, prop, value): + status.text = "Step 4: Moving up - Complete!" + target.fill_color = mcrfpy.Color(255, 255, 100) + target.animate("y", 100, 1.0, mcrfpy.Easing.EASE_IN_OUT) + +def restart_animation(timer, runtime): + frame.x = 100 + frame.y = 100 + frame.fill_color = mcrfpy.Color(255, 100, 100) + status.text = "Step 1: Moving right" + frame.animate("x", 824, 1.0, mcrfpy.Easing.EASE_IN_OUT, callback=step2) + +# Start chain +frame.animate("x", 824, 1.0, mcrfpy.Easing.EASE_IN_OUT, callback=step2) +loop_timer = mcrfpy.Timer("loop", restart_animation, 9000) # 4s animation + 5s pause diff --git a/tests/snippets/046_timer_basic.py b/tests/snippets/046_timer_basic.py new file mode 100644 index 0000000..0986244 --- /dev/null +++ b/tests/snippets/046_timer_basic.py @@ -0,0 +1,32 @@ +# mcrf: objects=[Caption,Color,Frame,Scene,Timer] verified=0.2.8-dev@a7ba486 status=ok +# Timer Basic - Periodic callback +import mcrfpy + +scene = mcrfpy.Scene("demo") +mcrfpy.current_scene = scene + +scene.children.append(mcrfpy.Frame( + pos=(0, 0), size=(1024, 768), + fill_color=mcrfpy.Color(30, 30, 40) +)) + +counter = mcrfpy.Caption( + text="Ticks: 0", + pos=(420, 350)) +counter.fill_color = mcrfpy.Color(255, 220, 100) +scene.children.append(counter) + +count = 0 + +def on_tick(timer, runtime): + global count + count += 1 + counter.text = f"Ticks: {count}" + +# Timer fires every 500ms +timer = mcrfpy.Timer("ticker", on_tick, 500) + +scene.children.append(mcrfpy.Caption( + text="Timer interval: 500ms", + pos=(410, 450) +)) diff --git a/tests/snippets/047_timer_once.py b/tests/snippets/047_timer_once.py new file mode 100644 index 0000000..144ca49 --- /dev/null +++ b/tests/snippets/047_timer_once.py @@ -0,0 +1,29 @@ +# mcrf: objects=[Caption,Color,Frame,Scene,Timer] verified=0.2.8-dev@a7ba486 status=ok +# Timer Once - Single-shot timer +import mcrfpy + +scene = mcrfpy.Scene("demo") +mcrfpy.current_scene = scene + +scene.children.append(mcrfpy.Frame( + pos=(0, 0), size=(1024, 768), + fill_color=mcrfpy.Color(30, 30, 40) +)) + +status = mcrfpy.Caption( + text="Waiting for timer...", + pos=(380, 350)) +status.fill_color = mcrfpy.Color(200, 200, 200) +scene.children.append(status) + +def on_fire(timer, runtime): + status.text = "Timer fired! (once=True)" + status.fill_color = mcrfpy.Color(100, 255, 100) + +# Timer fires once after 2 seconds +timer = mcrfpy.Timer("delayed", on_fire, 2000, once=True) + +scene.children.append(mcrfpy.Caption( + text="Single-shot timer fires after 2 seconds", + pos=(320, 450) +)) diff --git a/tests/snippets/048_timer_control.py b/tests/snippets/048_timer_control.py new file mode 100644 index 0000000..bf91526 --- /dev/null +++ b/tests/snippets/048_timer_control.py @@ -0,0 +1,51 @@ +# mcrf: objects=[Caption,Color,Frame,InputState,Key,Scene,Timer] verified=0.2.8-dev@a7ba486 status=ok +# Timer Control - Pause, resume, stop +import mcrfpy + +scene = mcrfpy.Scene("demo") +mcrfpy.current_scene = scene + +scene.children.append(mcrfpy.Frame( + pos=(0, 0), size=(1024, 768), + fill_color=mcrfpy.Color(30, 30, 40) +)) + +# Moving frame +frame = mcrfpy.Frame( + pos=(100, 334), size=(80, 80), + fill_color=mcrfpy.Color(255, 150, 100) +) +scene.children.append(frame) + +status = mcrfpy.Caption( + text="Timer running - P=pause, R=resume, S=stop", + pos=(280, 500) +) +scene.children.append(status) + +direction = 1 + +def animate(timer, runtime): + global direction + frame.x += 5 * direction + if frame.x > 844: + direction = -1 + elif frame.x < 100: + direction = 1 + +timer = mcrfpy.Timer("mover", animate, 16) + +def on_key(key, action): + if action != mcrfpy.InputState.PRESSED: + return + if key == mcrfpy.Key.P: + timer.pause() + status.text = "Timer PAUSED" + elif key == mcrfpy.Key.R: + timer.resume() + status.text = "Timer RESUMED" + elif key == mcrfpy.Key.S: + timer.stop() + status.text = "Timer STOPPED" + +scene.on_key = on_key diff --git a/tests/snippets/049_input_keyboard.py b/tests/snippets/049_input_keyboard.py new file mode 100644 index 0000000..e8c9374 --- /dev/null +++ b/tests/snippets/049_input_keyboard.py @@ -0,0 +1,38 @@ +# mcrf: objects=[Caption,Color,Frame,InputState,Scene] verified=0.2.8-dev@a7ba486 status=ok +# Input Keyboard - Key press handling +import mcrfpy + +scene = mcrfpy.Scene("demo") +mcrfpy.current_scene = scene + +scene.children.append(mcrfpy.Frame( + pos=(0, 0), size=(1024, 768), + fill_color=mcrfpy.Color(30, 30, 40) +)) + +key_display = mcrfpy.Caption( + text="Press any key...", + pos=(380, 300)) +key_display.fill_color = mcrfpy.Color(255, 220, 100) +scene.children.append(key_display) + +state_display = mcrfpy.Caption( + text="State: waiting", + pos=(420, 400)) +state_display.fill_color = mcrfpy.Color(200, 200, 200) +scene.children.append(state_display) + +def on_key(key, action): + key_display.text = f"Key: {key}" + + if action == mcrfpy.InputState.PRESSED: + state_display.text = "State: PRESSED" + state_display.fill_color = mcrfpy.Color(100, 255, 100) + elif action == mcrfpy.InputState.RELEASED: + state_display.text = "State: RELEASED" + state_display.fill_color = mcrfpy.Color(255, 100, 100) + elif action == mcrfpy.InputState.HELD: + state_display.text = "State: HELD" + state_display.fill_color = mcrfpy.Color(255, 255, 100) + +scene.on_key = on_key diff --git a/tests/snippets/050_input_mouse_click.py b/tests/snippets/050_input_mouse_click.py new file mode 100644 index 0000000..6b18840 --- /dev/null +++ b/tests/snippets/050_input_mouse_click.py @@ -0,0 +1,41 @@ +# mcrf: objects=[Caption,Color,Frame,InputState,Scene] verified=0.2.8-dev@a7ba486 status=ok +# Input Mouse Click - Click on frames +import mcrfpy + +scene = mcrfpy.Scene("demo") +mcrfpy.current_scene = scene + +scene.children.append(mcrfpy.Frame( + pos=(0, 0), size=(1024, 768), + fill_color=mcrfpy.Color(30, 30, 40) +)) + +# Clickable button +button = mcrfpy.Frame( + pos=(362, 284), size=(300, 200), + fill_color=mcrfpy.Color(100, 100, 200), + outline=3.0, + outline_color=mcrfpy.Color(150, 150, 255) +) +scene.children.append(button) + +label = mcrfpy.Caption( + text="Click me!", + pos=(100, 80) +) +button.children.append(label) + +click_count = 0 +status = mcrfpy.Caption(text="Clicks: 0", pos=(450, 550)) +scene.children.append(status) + +def on_click(pos, button_type, action): + global click_count + if action == mcrfpy.InputState.PRESSED: + click_count += 1 + status.text = f"Clicks: {click_count}" + button.fill_color = mcrfpy.Color(150, 150, 255) + elif action == mcrfpy.InputState.RELEASED: + button.fill_color = mcrfpy.Color(100, 100, 200) + +button.on_click = on_click diff --git a/tests/snippets/051_input_mouse_hover.py b/tests/snippets/051_input_mouse_hover.py new file mode 100644 index 0000000..589abff --- /dev/null +++ b/tests/snippets/051_input_mouse_hover.py @@ -0,0 +1,45 @@ +# mcrf: objects=[Caption,Color,Frame,Scene] verified=0.2.8-dev@a7ba486 status=ok +# Input Mouse Hover - Enter/exit/move events +import mcrfpy + +scene = mcrfpy.Scene("demo") +mcrfpy.current_scene = scene + +scene.children.append(mcrfpy.Frame( + pos=(0, 0), size=(1024, 768), + fill_color=mcrfpy.Color(30, 30, 40) +)) + +# Hoverable frame +frame = mcrfpy.Frame( + pos=(312, 234), size=(400, 300), + fill_color=mcrfpy.Color(80, 80, 120), + outline=3.0, + outline_color=mcrfpy.Color(120, 120, 180) +) +scene.children.append(frame) + +status = mcrfpy.Caption( + text="Mouse outside", + pos=(140, 130) +) +frame.children.append(status) + +coords = mcrfpy.Caption(text="", pos=(140, 180)) +frame.children.append(coords) + +def on_enter(pos): + frame.fill_color = mcrfpy.Color(100, 150, 100) + status.text = "Mouse ENTERED" + +def on_exit(pos): + frame.fill_color = mcrfpy.Color(80, 80, 120) + status.text = "Mouse EXITED" + coords.text = "" + +def on_move(pos): + coords.text = f"Position: ({pos.x:.0f}, {pos.y:.0f})" + +frame.on_enter = on_enter +frame.on_exit = on_exit +frame.on_move = on_move diff --git a/tests/snippets/052_input_grid_cell_click.py b/tests/snippets/052_input_grid_cell_click.py new file mode 100644 index 0000000..b2fe014 --- /dev/null +++ b/tests/snippets/052_input_grid_cell_click.py @@ -0,0 +1,47 @@ +# mcrf: objects=[Caption,Color,ColorLayer,Grid,InputState,Scene] verified=0.2.8-dev@a7ba486 status=ok +# Input Grid Cell Click - Click on grid cells +import mcrfpy + +scene = mcrfpy.Scene("demo") +mcrfpy.current_scene = scene + +grid = mcrfpy.Grid( + grid_size=(16, 12), + texture=mcrfpy.default_texture, + pos=(0, 0), + size=(1024, 768), + zoom=4.0 +) +scene.children.append(grid) + +# Add color layer for click highlighting +click_layer = mcrfpy.ColorLayer(z_index=1, name="clicks") +grid.add_layer(click_layer) + +# Fill with floor +for y in range(12): + for x in range(16): + grid.at(x, y).tilesprite = 48 + +status = mcrfpy.Caption(text="Click on cells to highlight", pos=(350, 720)) +status.outline = 2 +status.outline_color = mcrfpy.Color(0, 0, 0) +scene.children.append(status) + +# Track clicked cells +clicked = set() + +def on_cell_click(cell_pos, button, action): + if action == mcrfpy.InputState.PRESSED: + x, y = int(cell_pos.x), int(cell_pos.y) + if (x, y) in clicked: + # Clear highlight + click_layer.set((x, y), mcrfpy.Color(0, 0, 0, 0)) + clicked.discard((x, y)) + else: + # Add highlight + click_layer.set((x, y), mcrfpy.Color(255, 200, 100, 180)) + clicked.add((x, y)) + status.text = f"Clicked cell ({x}, {y})" + +grid.on_cell_click = on_cell_click diff --git a/tests/snippets/053_input_grid_cell_hover.py b/tests/snippets/053_input_grid_cell_hover.py new file mode 100644 index 0000000..2e3063d --- /dev/null +++ b/tests/snippets/053_input_grid_cell_hover.py @@ -0,0 +1,40 @@ +# mcrf: objects=[Caption,Color,ColorLayer,Grid,Scene] verified=0.2.8-dev@a7ba486 status=ok +# Input Grid Cell Hover - Hover over grid cells +import mcrfpy + +scene = mcrfpy.Scene("demo") +mcrfpy.current_scene = scene + +grid = mcrfpy.Grid( + grid_size=(16, 12), + texture=mcrfpy.default_texture, + pos=(0, 0), + size=(1024, 768), + zoom=4.0 +) +scene.children.append(grid) + +# Add color layer for hover highlighting +hover_layer = mcrfpy.ColorLayer(z_index=1, name="hover") +grid.add_layer(hover_layer) + +for y in range(12): + for x in range(16): + grid.at(x, y).tilesprite = 48 + +status = mcrfpy.Caption(text="Hover over cells", pos=(420, 720)) +status.outline = 2 +status.outline_color = mcrfpy.Color(0, 0, 0) +scene.children.append(status) + +def on_cell_enter(cell_pos): + x, y = int(cell_pos.x), int(cell_pos.y) + hover_layer.set((x, y), mcrfpy.Color(100, 200, 255, 180)) + status.text = f"Hovering cell ({x}, {y})" + +def on_cell_exit(cell_pos): + x, y = int(cell_pos.x), int(cell_pos.y) + hover_layer.set((x, y), mcrfpy.Color(0, 0, 0, 0)) + +grid.on_cell_enter = on_cell_enter +grid.on_cell_exit = on_cell_exit diff --git a/tests/snippets/054_pathfinding_astar.py b/tests/snippets/054_pathfinding_astar.py new file mode 100644 index 0000000..5508227 --- /dev/null +++ b/tests/snippets/054_pathfinding_astar.py @@ -0,0 +1,56 @@ +# mcrf: objects=[Caption,Color,ColorLayer,Grid,Scene] verified=0.2.8-dev@a7ba486 status=ok +# Pathfinding A* - Find path between points +import mcrfpy + +scene = mcrfpy.Scene("demo") +mcrfpy.current_scene = scene + +grid = mcrfpy.Grid( + grid_size=(16, 12), + texture=mcrfpy.default_texture, + pos=(0, 0), + size=(1024, 768), + zoom=4.0 +) +scene.children.append(grid) + +# Add color layer for path visualization +path_layer = mcrfpy.ColorLayer(z_index=1, name="path") +grid.add_layer(path_layer) + +# Create maze +for y in range(12): + for x in range(16): + cell = grid.at(x, y) + # Border walls + if x == 0 or x == 15 or y == 0 or y == 11: + cell.tilesprite = 1 + cell.walkable = False + # Some obstacles + elif x == 8 and y > 1 and y < 10: + cell.tilesprite = 1 + cell.walkable = False + elif x == 4 and y > 3 and y < 11: + cell.tilesprite = 1 + cell.walkable = False + else: + cell.tilesprite = 48 + cell.walkable = True + +# Find path +start = (2, 6) +end = (13, 6) +path = grid.find_path(start, end) + +# Highlight path using color layer +for px, py in path: + path_layer.set((px, py), mcrfpy.Color(100, 255, 100, 180)) + +# Mark start/end +path_layer.set((start[0], start[1]), mcrfpy.Color(100, 100, 255, 200)) +path_layer.set((end[0], end[1]), mcrfpy.Color(255, 100, 100, 200)) + +status = mcrfpy.Caption(text=f"A* Path length: {len(path)} - Blue=start, Red=end", pos=(320, 700)) +status.outline = 2 +status.outline_color = mcrfpy.Color(0, 0, 0) +scene.children.append(status) diff --git a/tests/snippets/055_pathfinding_entity.py b/tests/snippets/055_pathfinding_entity.py new file mode 100644 index 0000000..53ad73d --- /dev/null +++ b/tests/snippets/055_pathfinding_entity.py @@ -0,0 +1,54 @@ +# mcrf: objects=[Caption,Color,ColorLayer,Entity,Grid,Scene] verified=0.2.8-dev@a7ba486 status=ok +# Entity Pathfinding - Entity path_to method +import mcrfpy + +scene = mcrfpy.Scene("demo") +mcrfpy.current_scene = scene + +grid = mcrfpy.Grid( + grid_size=(16, 12), + texture=mcrfpy.default_texture, + pos=(0, 0), + size=(1024, 768), + zoom=4.0 +) +scene.children.append(grid) + +# Add color layer for path visualization +path_layer = mcrfpy.ColorLayer(z_index=1, name="path") +grid.add_layer(path_layer) + +# Floor with some walls +for y in range(12): + for x in range(16): + cell = grid.at(x, y) + if x == 8 and y > 1 and y < 10: + cell.tilesprite = 1 + cell.walkable = False + else: + cell.tilesprite = 48 + cell.walkable = True + +# Entity +player = mcrfpy.Entity( + grid_pos=(3, 6), + texture=mcrfpy.default_texture, + sprite_index=84 +) +grid.entities.append(player) + +# Find path to target +target = (13, 6) +path = player.path_to(target) + +# Visualize path using color layer +for px, py in path: + path_layer.set((px, py), mcrfpy.Color(150, 200, 255, 180)) + +# Mark target +path_layer.set((target[0], target[1]), mcrfpy.Color(255, 200, 100, 200)) + +status = mcrfpy.Caption(text="Entity pathfinding around wall", pos=(360, 720)) +status.outline = 2 +status.outline_color = mcrfpy.Color(0, 0, 0) +scene.children.append(status) diff --git a/tests/snippets/056_pathfinding_dijkstra.py b/tests/snippets/056_pathfinding_dijkstra.py new file mode 100644 index 0000000..0895aa9 --- /dev/null +++ b/tests/snippets/056_pathfinding_dijkstra.py @@ -0,0 +1,52 @@ +# mcrf: objects=[Caption,Color,ColorLayer,Grid,Scene] verified=0.2.8-dev@a7ba486 status=ok +# Dijkstra Map - Distance-based pathfinding +import mcrfpy + +scene = mcrfpy.Scene("demo") +mcrfpy.current_scene = scene + +grid = mcrfpy.Grid( + grid_size=(16, 12), + texture=mcrfpy.default_texture, + pos=(0, 0), + size=(1024, 768), + zoom=4.0 +) +scene.children.append(grid) + +# Add color layer for visualization +dist_layer = mcrfpy.ColorLayer(z_index=1, name="distances") +grid.add_layer(dist_layer) + +# Floor tiles +for y in range(12): + for x in range(16): + cell = grid.at(x, y) + cell.tilesprite = 48 + cell.walkable = True + +# Some walls +for i in range(5): + grid.at(8, 2 + i).tilesprite = 1 + grid.at(8, 2 + i).walkable = False + +# Compute dijkstra from center +center = (8, 6) +dijkstra = grid.get_dijkstra_map(center) + +# Visualize distances with color gradient +for y in range(12): + for x in range(16): + if grid.at(x, y).walkable: + dist = dijkstra.distance((x, y)) + if dist is not None and dist < 1000: # Valid distance + intensity = max(0, 255 - int(dist * 20)) + dist_layer.set((x, y), mcrfpy.Color(intensity, intensity, 255, 180)) + +# Mark center +dist_layer.set((center[0], center[1]), mcrfpy.Color(255, 100, 100, 255)) + +status = mcrfpy.Caption(text="Dijkstra distance map (darker = farther)", pos=(320, 720)) +status.outline = 2 +status.outline_color = mcrfpy.Color(0, 0, 0) +scene.children.append(status) diff --git a/tests/snippets/057_fov_basic.py b/tests/snippets/057_fov_basic.py new file mode 100644 index 0000000..d84bc89 --- /dev/null +++ b/tests/snippets/057_fov_basic.py @@ -0,0 +1,58 @@ +# mcrf: objects=[Caption,Color,ColorLayer,Entity,Grid,Scene] verified=0.2.8-dev@a7ba486 status=ok +# FOV Basic - Field of view computation +import mcrfpy + +scene = mcrfpy.Scene("demo") +mcrfpy.current_scene = scene + +grid = mcrfpy.Grid( + grid_size=(16, 12), + texture=mcrfpy.default_texture, + pos=(0, 0), + size=(1024, 768), + zoom=4.0 +) +scene.children.append(grid) + +# Add fog layer for FOV visualization +fog_layer = mcrfpy.ColorLayer(z_index=1, name="fog") +grid.add_layer(fog_layer) +fog_layer.fill(mcrfpy.Color(0, 0, 0, 200)) # Start dark + +# Create dungeon with walls +for y in range(12): + for x in range(16): + cell = grid.at(x, y) + # Border walls + if x == 0 or x == 15 or y == 0 or y == 11: + cell.tilesprite = 1 + cell.walkable = False + cell.transparent = False + # Internal pillars + elif (x in [4, 8, 12]) and (y in [4, 8]): + cell.tilesprite = 1 + cell.walkable = False + cell.transparent = False + else: + cell.tilesprite = 48 + cell.walkable = True + cell.transparent = True + +# Player position +player_pos = (8, 6) +player = mcrfpy.Entity(grid_pos=player_pos, texture=mcrfpy.default_texture, sprite_index=84) +grid.entities.append(player) + +# Compute FOV from player position +grid.compute_fov(player_pos, radius=8) + +# Clear fog for visible cells +for y in range(12): + for x in range(16): + if grid.is_in_fov((x, y)): + fog_layer.set((x, y), mcrfpy.Color(0, 0, 0, 0)) # Clear + +status = mcrfpy.Caption(text="FOV radius=8, dark areas not visible", pos=(340, 700)) +status.outline = 2 +status.outline_color = mcrfpy.Color(0, 0, 0) +scene.children.append(status) diff --git a/tests/snippets/058_fov_algorithms.py b/tests/snippets/058_fov_algorithms.py new file mode 100644 index 0000000..cd8b5ac --- /dev/null +++ b/tests/snippets/058_fov_algorithms.py @@ -0,0 +1,60 @@ +# mcrf: objects=[Caption,Color,ColorLayer,FOV,Frame,Grid,Scene] verified=0.2.8-dev@a7ba486 status=ok +# FOV Algorithms - Compare different algorithms +import mcrfpy + +scene = mcrfpy.Scene("demo") +mcrfpy.current_scene = scene + +scene.children.append(mcrfpy.Frame(pos=(0, 0), size=(1024, 768), fill_color=mcrfpy.Color(30, 30, 40))) + +algorithms = [ + (mcrfpy.FOV.BASIC, "BASIC"), + (mcrfpy.FOV.SHADOW, "SHADOW"), + (mcrfpy.FOV.DIAMOND, "DIAMOND"), + (mcrfpy.FOV.PERMISSIVE_4, "PERMISSIVE4"), +] + +for i, (algo, name) in enumerate(algorithms): + x = 62 + (i % 2) * 480 + y = 60 + (i // 2) * 360 + + grid = mcrfpy.Grid( + grid_size=(12, 9), + texture=mcrfpy.default_texture, + pos=(x, y), + size=(420, 300) + ) + scene.children.append(grid) + + # Add fog layer + fog = mcrfpy.ColorLayer(z_index=1, name="fog") + grid.add_layer(fog) + fog.fill(mcrfpy.Color(0, 0, 0, 180)) + + # Setup dungeon + for gy in range(9): + for gx in range(12): + cell = grid.at(gx, gy) + if gx == 0 or gx == 11 or gy == 0 or gy == 8: + cell.tilesprite = 1 + cell.transparent = False + elif gx == 6 and gy > 1 and gy < 7: + cell.tilesprite = 1 + cell.transparent = False + else: + cell.tilesprite = 48 + cell.transparent = True + + # Compute FOV with this algorithm + grid.compute_fov((3, 4), radius=6, algorithm=algo) + + # Clear fog for visible cells + for gy in range(9): + for gx in range(12): + if grid.is_in_fov((gx, gy)): + fog.set((gx, gy), mcrfpy.Color(0, 0, 0, 0)) + + label = mcrfpy.Caption(text=name, pos=(x + 170, y + 310)) + label.outline = 2 + label.outline_color = mcrfpy.Color(0, 0, 0) + scene.children.append(label) diff --git a/tests/snippets/059_fov_perspective.py b/tests/snippets/059_fov_perspective.py new file mode 100644 index 0000000..5d9785f --- /dev/null +++ b/tests/snippets/059_fov_perspective.py @@ -0,0 +1,53 @@ +# mcrf: objects=[Caption,Color,Entity,Grid,InputState,Key,Scene] verified=0.2.8-dev@a7ba486 status=ok +# FOV Perspective Entity - View from entity's perspective +import mcrfpy + +scene = mcrfpy.Scene("demo") +mcrfpy.current_scene = scene + +grid = mcrfpy.Grid( + grid_size=(16, 12), + texture=mcrfpy.default_texture, + pos=(0, 0), + size=(1024, 768), + zoom=4.0 +) +scene.children.append(grid) + +# Dungeon layout +for y in range(12): + for x in range(16): + cell = grid.at(x, y) + if x == 0 or x == 15 or y == 0 or y == 11: + cell.tilesprite = 1 + cell.transparent = False + elif (x == 4 or x == 12) and y > 2 and y < 9: + cell.tilesprite = 1 + cell.transparent = False + else: + cell.tilesprite = 48 + cell.transparent = True + +# Player entity +player = mcrfpy.Entity(grid_pos=(8, 6), texture=mcrfpy.default_texture, sprite_index=84) +grid.entities.append(player) + +# Set perspective entity - FOV follows this entity +grid.perspective = player +grid.fov_radius = 6 + +status = mcrfpy.Caption(text="WASD to move, FOV follows player", pos=(350, 700)) +status.outline = 2 +status.outline_color = mcrfpy.Color(0, 0, 0) +scene.children.append(status) + +def on_key(key, action): + if action != mcrfpy.InputState.PRESSED: + return + x, y = player.grid_x, player.grid_y + if key == mcrfpy.Key.W: player.grid_pos = (x, y - 1) + elif key == mcrfpy.Key.S: player.grid_pos = (x, y + 1) + elif key == mcrfpy.Key.A: player.grid_pos = (x - 1, y) + elif key == mcrfpy.Key.D: player.grid_pos = (x + 1, y) + +scene.on_key = on_key diff --git a/tests/snippets/060_bsp_basic.py b/tests/snippets/060_bsp_basic.py new file mode 100644 index 0000000..1c28468 --- /dev/null +++ b/tests/snippets/060_bsp_basic.py @@ -0,0 +1,56 @@ +# mcrf: objects=[BSP,Caption,Color,ColorLayer,Grid,Scene] verified=0.2.8-dev@a7ba486 status=ok +# BSP Basic - Binary space partition for dungeon generation +import mcrfpy + +scene = mcrfpy.Scene("demo") +mcrfpy.current_scene = scene + +grid = mcrfpy.Grid( + grid_size=(16, 12), + texture=mcrfpy.default_texture, + pos=(0, 0), + size=(1024, 768), + zoom=4.0 +) +scene.children.append(grid) + +# Add color layer for room visualization +room_layer = mcrfpy.ColorLayer(z_index=1, name="rooms") +grid.add_layer(room_layer) + +# Fill with walls +for y in range(12): + for x in range(16): + grid.at(x, y).tilesprite = 1 + grid.at(x, y).walkable = False + +# Create BSP tree (pos and size as tuples) +bsp = mcrfpy.BSP(pos=(0, 0), size=(16, 12)) +bsp.split_recursive(depth=3, min_size=(3, 3)) + +# Room colors +colors = [ + mcrfpy.Color(255, 150, 150, 150), + mcrfpy.Color(150, 255, 150, 150), + mcrfpy.Color(150, 150, 255, 150), + mcrfpy.Color(255, 255, 150, 150), + mcrfpy.Color(255, 150, 255, 150), + mcrfpy.Color(150, 255, 255, 150), +] + +# Carve rooms from leaves +for i, leaf in enumerate(bsp): + x, y = leaf.pos + w, h = leaf.size + # Shrink room slightly + for ry in range(y + 1, y + h - 1): + for rx in range(x + 1, x + w - 1): + if 0 <= rx < 16 and 0 <= ry < 12: + grid.at(rx, ry).tilesprite = 48 + grid.at(rx, ry).walkable = True + room_layer.set((rx, ry), colors[i % len(colors)]) + +status = mcrfpy.Caption(text="BSP dungeon - each color is a leaf node", pos=(320, 720)) +status.outline = 2 +status.outline_color = mcrfpy.Color(0, 0, 0) +scene.children.append(status) diff --git a/tests/snippets/061_scene_switching.py b/tests/snippets/061_scene_switching.py new file mode 100644 index 0000000..b69525f --- /dev/null +++ b/tests/snippets/061_scene_switching.py @@ -0,0 +1,34 @@ +# mcrf: objects=[Caption,Color,Frame,InputState,Key,Scene] verified=0.2.8-dev@a7ba486 status=ok +# Scene Switching - Multiple scenes +import mcrfpy + +# Create two scenes +scene1 = mcrfpy.Scene("menu") +scene2 = mcrfpy.Scene("game") + +# Menu scene +scene1.children.append(mcrfpy.Frame(pos=(0, 0), size=(1024, 768), fill_color=mcrfpy.Color(50, 50, 80))) +_caption = mcrfpy.Caption(text="MENU SCENE", pos=(420, 300)) +_caption.fill_color = mcrfpy.Color(255, 220, 100) +scene1.children.append(_caption) +scene1.children.append(mcrfpy.Caption(text="Press SPACE to go to game", pos=(360, 400))) + +# Game scene +scene2.children.append(mcrfpy.Frame(pos=(0, 0), size=(1024, 768), fill_color=mcrfpy.Color(80, 50, 50))) +_caption = mcrfpy.Caption(text="GAME SCENE", pos=(420, 300)) +_caption.fill_color = mcrfpy.Color(100, 255, 100) +scene2.children.append(_caption) +scene2.children.append(mcrfpy.Caption(text="Press SPACE to go to menu", pos=(360, 400))) + +def menu_key(key, action): + if key == mcrfpy.Key.SPACE and action == mcrfpy.InputState.PRESSED: + scene2.activate() + +def game_key(key, action): + if key == mcrfpy.Key.SPACE and action == mcrfpy.InputState.PRESSED: + scene1.activate() + +scene1.on_key = menu_key +scene2.on_key = game_key + +mcrfpy.current_scene = scene1 diff --git a/tests/snippets/062_scene_transition_fade.py b/tests/snippets/062_scene_transition_fade.py new file mode 100644 index 0000000..c28a3e7 --- /dev/null +++ b/tests/snippets/062_scene_transition_fade.py @@ -0,0 +1,36 @@ +# mcrf: objects=[Caption,Color,Frame,InputState,Key,Scene,Transition] verified=0.2.8-dev@a7ba486 status=ok +# Scene Transition Fade - Smooth scene transitions +import mcrfpy + +scene1 = mcrfpy.Scene("scene1") +scene2 = mcrfpy.Scene("scene2") + +scene1.children.append(mcrfpy.Frame(pos=(0, 0), size=(1024, 768), fill_color=mcrfpy.Color(100, 50, 50))) +_caption = mcrfpy.Caption(text="Scene 1 - Red", pos=(420, 350)) +_caption.fill_color = mcrfpy.Color(255, 255, 255) +scene1.children.append(_caption) + +scene2.children.append(mcrfpy.Frame(pos=(0, 0), size=(1024, 768), fill_color=mcrfpy.Color(50, 50, 100))) +_caption = mcrfpy.Caption(text="Scene 2 - Blue", pos=(420, 350)) +_caption.fill_color = mcrfpy.Color(255, 255, 255) +scene2.children.append(_caption) + +# Set default transition +mcrfpy.default_transition = mcrfpy.Transition.FADE +mcrfpy.default_transition_duration = 1.0 + +scene1.children.append(mcrfpy.Caption(text="Press SPACE - Fade transition", pos=(340, 450))) +scene2.children.append(mcrfpy.Caption(text="Press SPACE - Fade transition", pos=(340, 450))) + +def key1(key, action): + if key == mcrfpy.Key.SPACE and action == mcrfpy.InputState.PRESSED: + scene2.activate() + +def key2(key, action): + if key == mcrfpy.Key.SPACE and action == mcrfpy.InputState.PRESSED: + scene1.activate() + +scene1.on_key = key1 +scene2.on_key = key2 + +mcrfpy.current_scene = scene1 diff --git a/tests/snippets/063_scene_transition_slide.py b/tests/snippets/063_scene_transition_slide.py new file mode 100644 index 0000000..db0f14e --- /dev/null +++ b/tests/snippets/063_scene_transition_slide.py @@ -0,0 +1,35 @@ +# mcrf: objects=[Caption,Color,Frame,InputState,Key,Scene,Transition] verified=0.2.8-dev@a7ba486 status=ok +# Scene Transition Slide - Sliding transitions +import mcrfpy + +scene1 = mcrfpy.Scene("left") +scene2 = mcrfpy.Scene("right") + +scene1.children.append(mcrfpy.Frame(pos=(0, 0), size=(1024, 768), fill_color=mcrfpy.Color(50, 100, 50))) +_caption = mcrfpy.Caption(text="LEFT Scene", pos=(420, 350)) +_caption.fill_color = mcrfpy.Color(255, 255, 255) +scene1.children.append(_caption) + +scene2.children.append(mcrfpy.Frame(pos=(0, 0), size=(1024, 768), fill_color=mcrfpy.Color(100, 50, 100))) +_caption = mcrfpy.Caption(text="RIGHT Scene", pos=(420, 350)) +_caption.fill_color = mcrfpy.Color(255, 255, 255) +scene2.children.append(_caption) + +mcrfpy.default_transition = mcrfpy.Transition.SLIDE_LEFT +mcrfpy.default_transition_duration = 0.5 + +scene1.children.append(mcrfpy.Caption(text="Press SPACE - Slide left", pos=(360, 450))) +scene2.children.append(mcrfpy.Caption(text="Press SPACE - Slide left", pos=(360, 450))) + +def key1(key, action): + if key == mcrfpy.Key.SPACE and action == mcrfpy.InputState.PRESSED: + scene2.activate() + +def key2(key, action): + if key == mcrfpy.Key.SPACE and action == mcrfpy.InputState.PRESSED: + scene1.activate() + +scene1.on_key = key1 +scene2.on_key = key2 + +mcrfpy.current_scene = scene1 diff --git a/tests/snippets/064_vector_basics.py b/tests/snippets/064_vector_basics.py new file mode 100644 index 0000000..fd6d0fc --- /dev/null +++ b/tests/snippets/064_vector_basics.py @@ -0,0 +1,50 @@ +# mcrf: objects=[Caption,Color,Frame,Scene,Vector] verified=0.2.8-dev@a7ba486 status=ok +# Vector Basics - 2D vector operations +import mcrfpy + +scene = mcrfpy.Scene("demo") +mcrfpy.current_scene = scene + +scene.children.append(mcrfpy.Frame(pos=(0, 0), size=(1024, 768), fill_color=mcrfpy.Color(30, 30, 40))) + +# Create vectors +v1 = mcrfpy.Vector(100, 50) +v2 = mcrfpy.Vector(30, 80) + +# Display vector info +info = [ + f"v1 = Vector({v1.x}, {v1.y})", + f"v2 = Vector({v2.x}, {v2.y})", + f"", + f"v1.magnitude() = {v1.magnitude():.2f}", + f"v1.angle() = {v1.angle():.2f} radians", + f"", + f"Distance v1 to v2 = {v1.distance_to(v2):.2f}", +] + +y = 150 +for line in info: + cap = mcrfpy.Caption(text=line, pos=(300, y)) + cap.fill_color = mcrfpy.Color(200, 200, 200) + scene.children.append(cap) + y += 60 + +# Visual representation +origin = mcrfpy.Frame(pos=(500, 500), size=(10, 10), fill_color=mcrfpy.Color(255, 255, 255)) +scene.children.append(origin) + +# v1 endpoint +p1 = mcrfpy.Frame(pos=(500 + v1.x * 2, 500 - v1.y * 2), size=(15, 15), fill_color=mcrfpy.Color(255, 100, 100)) +scene.children.append(p1) +v1_label = mcrfpy.Caption(text="v1", pos=(500 + v1.x * 2 + 20, 500 - v1.y * 2)) +v1_label.outline = 2 +v1_label.outline_color = mcrfpy.Color(0, 0, 0) +scene.children.append(v1_label) + +# v2 endpoint +p2 = mcrfpy.Frame(pos=(500 + v2.x * 2, 500 - v2.y * 2), size=(15, 15), fill_color=mcrfpy.Color(100, 100, 255)) +scene.children.append(p2) +v2_label = mcrfpy.Caption(text="v2", pos=(500 + v2.x * 2 + 20, 500 - v2.y * 2)) +v2_label.outline = 2 +v2_label.outline_color = mcrfpy.Color(0, 0, 0) +scene.children.append(v2_label) diff --git a/tests/snippets/065_color_components.py b/tests/snippets/065_color_components.py new file mode 100644 index 0000000..bb7760f --- /dev/null +++ b/tests/snippets/065_color_components.py @@ -0,0 +1,40 @@ +# mcrf: objects=[Caption,Color,Frame,Scene] verified=0.2.8-dev@a7ba486 status=ok +# Color Components - RGBA color manipulation +import mcrfpy + +scene = mcrfpy.Scene("demo") +mcrfpy.current_scene = scene + +scene.children.append(mcrfpy.Frame(pos=(0, 0), size=(1024, 768), fill_color=mcrfpy.Color(30, 30, 40))) + +# Show color components +colors = [ + (mcrfpy.Color(255, 0, 0), "Pure Red"), + (mcrfpy.Color(0, 255, 0), "Pure Green"), + (mcrfpy.Color(0, 0, 255), "Pure Blue"), + (mcrfpy.Color(255, 255, 0), "Yellow (R+G)"), + (mcrfpy.Color(255, 0, 255), "Magenta (R+B)"), + (mcrfpy.Color(0, 255, 255), "Cyan (G+B)"), + (mcrfpy.Color(128, 128, 128), "Gray"), + (mcrfpy.Color(255, 255, 255, 128), "White 50% Alpha"), +] + +for i, (color, name) in enumerate(colors): + x = 100 + (i % 4) * 230 + y = 150 + (i // 4) * 280 + + frame = mcrfpy.Frame(pos=(x, y), size=(180, 100), fill_color=color) + scene.children.append(frame) + + label = mcrfpy.Caption(text=name, pos=(x, y + 110)) + scene.children.append(label) + + rgba = mcrfpy.Caption( + text=f"R:{color.r} G:{color.g} B:{color.b} A:{color.a}", + pos=(x, y + 140)) + rgba.fill_color = mcrfpy.Color(150, 150, 150) + scene.children.append(rgba) + +_caption = mcrfpy.Caption(text="Color RGBA Components", pos=(400, 60)) +_caption.fill_color = mcrfpy.Color(255, 220, 100) +scene.children.append(_caption) diff --git a/tests/snippets/066_find_elements.py b/tests/snippets/066_find_elements.py new file mode 100644 index 0000000..80f5f13 --- /dev/null +++ b/tests/snippets/066_find_elements.py @@ -0,0 +1,36 @@ +# mcrf: objects=[Caption,Color,Frame,Scene] verified=0.2.8-dev@a7ba486 status=ok +# Find Elements - Locate UI by name +import mcrfpy + +scene = mcrfpy.Scene("demo") +mcrfpy.current_scene = scene + +scene.children.append(mcrfpy.Frame(pos=(0, 0), size=(1024, 768), fill_color=mcrfpy.Color(30, 30, 40))) + +# Create named elements +elements = [ + ("btn_play", 200, 200, mcrfpy.Color(100, 150, 100)), + ("btn_options", 200, 320, mcrfpy.Color(100, 100, 150)), + ("btn_quit", 200, 440, mcrfpy.Color(150, 100, 100)), + ("header", 500, 100, mcrfpy.Color(150, 150, 100)), +] + +for name, x, y, color in elements: + frame = mcrfpy.Frame(pos=(x, y), size=(200, 80), fill_color=color, name=name) + scene.children.append(frame) + label = mcrfpy.Caption(text=name, pos=(20, 30)) + frame.children.append(label) + +# Find specific element +found = mcrfpy.find("btn_options") +if found: + found.outline = 4.0 + found.outline_color = mcrfpy.Color(255, 255, 255) + +# Find all buttons +buttons = mcrfpy.find_all("btn_*") +status = mcrfpy.Caption( + text=f"Found {len(buttons)} elements matching 'btn_*'", + pos=(350, 600)) +status.fill_color = mcrfpy.Color(200, 200, 200) +scene.children.append(status) diff --git a/tests/snippets/067_collection_iteration.py b/tests/snippets/067_collection_iteration.py new file mode 100644 index 0000000..6c21071 --- /dev/null +++ b/tests/snippets/067_collection_iteration.py @@ -0,0 +1,40 @@ +# mcrf: objects=[Caption,Color,Frame,Scene] verified=0.2.8-dev@a7ba486 status=ok +# Collection Iteration - Loop through UI children +import mcrfpy + +scene = mcrfpy.Scene("demo") +mcrfpy.current_scene = scene + +scene.children.append(mcrfpy.Frame(pos=(0, 0), size=(1024, 768), fill_color=mcrfpy.Color(30, 30, 40))) + +# Container with multiple children +container = mcrfpy.Frame(pos=(162, 134), size=(700, 500), fill_color=mcrfpy.Color(50, 50, 70)) +scene.children.append(container) + +# Add various elements +for i in range(5): + frame = mcrfpy.Frame( + pos=(20 + i * 130, 50), + size=(110, 80), + fill_color=mcrfpy.Color(100 + i * 30, 100, 200 - i * 30) + ) + container.children.append(frame) + +for i in range(3): + cap = mcrfpy.Caption(text=f"Label {i}", pos=(50 + i * 200, 200)) + container.children.append(cap) + +# Iterate and count by type +frames = 0 +captions = 0 +for child in container.children: + if hasattr(child, 'fill_color'): + frames += 1 + if hasattr(child, 'text'): + captions += 1 + +status = mcrfpy.Caption( + text=f"Container has {len(container.children)} children: {frames} frames, {captions} captions", + pos=(100, 400) +) +container.children.append(status) diff --git a/tests/snippets/068_entity_collection.py b/tests/snippets/068_entity_collection.py new file mode 100644 index 0000000..9a8ce05 --- /dev/null +++ b/tests/snippets/068_entity_collection.py @@ -0,0 +1,41 @@ +# mcrf: objects=[Caption,Color,Entity,Grid,Scene] verified=0.2.8-dev@a7ba486 status=ok +# Entity Collection - Manage grid entities +import mcrfpy + +scene = mcrfpy.Scene("demo") +mcrfpy.current_scene = scene + +grid = mcrfpy.Grid( + grid_size=(16, 12), + texture=mcrfpy.default_texture, + pos=(0, 0), + size=(1024, 768), + zoom=4.0 +) +scene.children.append(grid) + +for y in range(12): + for x in range(16): + grid.at(x, y).tilesprite = 48 + +# Add multiple entities +sprites = [84, 86, 88, 112, 116, 120] +for i, sprite_idx in enumerate(sprites): + entity = mcrfpy.Entity( + grid_pos=(2 + i * 2, 6), + texture=mcrfpy.default_texture, + sprite_index=sprite_idx, + name=f"entity_{i}" + ) + grid.entities.append(entity) + +# Count and iterate +status = mcrfpy.Caption(text=f"Grid has {len(grid.entities)} entities", pos=(400, 720)) +status.outline = 2 +status.outline_color = mcrfpy.Color(0, 0, 0) +scene.children.append(status) + +# Highlight alternating entities +for i, entity in enumerate(grid.entities): + if i % 2 == 0: + entity.opacity = 0.5 diff --git a/tests/snippets/069_window_properties.py b/tests/snippets/069_window_properties.py new file mode 100644 index 0000000..3602b9b --- /dev/null +++ b/tests/snippets/069_window_properties.py @@ -0,0 +1,33 @@ +# mcrf: objects=[Caption,Color,Frame,Scene] verified=0.2.8-dev@a7ba486 status=ok +# Window Properties - Access window settings +import mcrfpy + +scene = mcrfpy.Scene("demo") +mcrfpy.current_scene = scene + +scene.children.append(mcrfpy.Frame(pos=(0, 0), size=(1024, 768), fill_color=mcrfpy.Color(30, 30, 40))) + +# Display window info +window = mcrfpy.window +info = [ + f"Resolution: {window.resolution}", + f"Game Resolution: {window.game_resolution}", + f"Title: {window.title}", + f"Fullscreen: {window.fullscreen}", + f"VSync: {window.vsync}", + f"Framerate Limit: {window.framerate_limit}", + f"Scaling Mode: {window.scaling_mode}", +] + +_caption = mcrfpy.Caption( + text="Window Properties", + pos=(400, 100)) +_caption.fill_color = mcrfpy.Color(255, 220, 100) +scene.children.append(_caption) + +y = 200 +for line in info: + cap = mcrfpy.Caption(text=line, pos=(300, y)) + cap.fill_color = mcrfpy.Color(200, 200, 200) + scene.children.append(cap) + y += 60 diff --git a/tests/snippets/070_keyboard_state.py b/tests/snippets/070_keyboard_state.py new file mode 100644 index 0000000..8e78c80 --- /dev/null +++ b/tests/snippets/070_keyboard_state.py @@ -0,0 +1,46 @@ +# mcrf: objects=[Caption,Color,Frame,InputState,Scene] verified=0.2.8-dev@a7ba486 status=ok +# Keyboard State - Track key presses +import mcrfpy + +scene = mcrfpy.Scene("demo") +mcrfpy.current_scene = scene + +scene.children.append(mcrfpy.Frame(pos=(0, 0), size=(1024, 768), fill_color=mcrfpy.Color(30, 30, 40))) + +_caption = mcrfpy.Caption(text="Keyboard State", pos=(420, 100)) +_caption.fill_color = mcrfpy.Color(255, 220, 100) +scene.children.append(_caption) + +last_key_label = mcrfpy.Caption(text="Last key: none", pos=(200, 250)) +scene.children.append(last_key_label) + +state_label = mcrfpy.Caption(text="State: waiting", pos=(200, 350)) +scene.children.append(state_label) + +modifier_label = mcrfpy.Caption(text="Modifiers: none", pos=(200, 450)) +scene.children.append(modifier_label) + +scene.children.append(mcrfpy.Caption(text="Press keys to see state", pos=(350, 550))) + +def on_key(key, action): + last_key_label.text = f"Last key: {key}" + + if action == mcrfpy.InputState.PRESSED: + state_label.text = "State: PRESSED" + state_label.fill_color = mcrfpy.Color(100, 255, 100) + elif action == mcrfpy.InputState.RELEASED: + state_label.text = "State: RELEASED" + state_label.fill_color = mcrfpy.Color(255, 100, 100) + elif action == mcrfpy.InputState.HELD: + state_label.text = "State: HELD" + state_label.fill_color = mcrfpy.Color(255, 255, 100) + + # Show modifiers + kb = mcrfpy.keyboard + mods = [] + if kb.ctrl: mods.append("CTRL") + if kb.shift: mods.append("SHIFT") + if kb.alt: mods.append("ALT") + modifier_label.text = f"Modifiers: {', '.join(mods) if mods else 'none'}" + +scene.on_key = on_key diff --git a/tests/snippets/071_mouse_state.py b/tests/snippets/071_mouse_state.py new file mode 100644 index 0000000..6f8315a --- /dev/null +++ b/tests/snippets/071_mouse_state.py @@ -0,0 +1,39 @@ +# mcrf: objects=[Caption,Color,Frame,Scene,Timer,Vector] verified=0.2.8-dev@a7ba486 status=ok +# Mouse State - Track mouse position and buttons +import mcrfpy + +scene = mcrfpy.Scene("demo") +mcrfpy.current_scene = scene + +scene.children.append(mcrfpy.Frame(pos=(0, 0), size=(1024, 768), fill_color=mcrfpy.Color(30, 30, 40))) + +_caption = mcrfpy.Caption(text="Mouse State", pos=(430, 100)) +_caption.fill_color = mcrfpy.Color(255, 220, 100) +scene.children.append(_caption) + +pos_label = mcrfpy.Caption(text="Position: (0, 0)", pos=(300, 250)) +scene.children.append(pos_label) + +button_label = mcrfpy.Caption(text="Buttons: none", pos=(300, 350)) +scene.children.append(button_label) + +# Cursor follower +cursor = mcrfpy.Frame(pos=(0, 0), size=(20, 20), fill_color=mcrfpy.Color(255, 100, 100)) +scene.children.append(cursor) + +def update_display(timer, runtime): + mouse = mcrfpy.mouse + pos = mouse.pos + + pos_label.text = f"Position: ({pos.x:.0f}, {pos.y:.0f})" + + # Check button states + buttons = [] + if mouse.left: buttons.append("LEFT") + if mouse.middle: buttons.append("MIDDLE") + if mouse.right: buttons.append("RIGHT") + button_label.text = f"Buttons: {', '.join(buttons) if buttons else 'none'}" + + cursor.pos = mcrfpy.Vector(pos.x - 10, pos.y - 10) + +timer = mcrfpy.Timer("mouse_poll", update_display, 16) diff --git a/tests/snippets/072_ui_button.py b/tests/snippets/072_ui_button.py new file mode 100644 index 0000000..4de63e3 --- /dev/null +++ b/tests/snippets/072_ui_button.py @@ -0,0 +1,47 @@ +# mcrf: objects=[Caption,Color,Frame,InputState,Scene] verified=0.2.8-dev@a7ba486 status=ok +# UI Button - Interactive button with hover +import mcrfpy + +scene = mcrfpy.Scene("demo") +mcrfpy.current_scene = scene + +scene.children.append(mcrfpy.Frame(pos=(0, 0), size=(1024, 768), fill_color=mcrfpy.Color(30, 30, 40))) + +def make_button(x, y, text, callback): + btn = mcrfpy.Frame( + pos=(x, y), size=(200, 60), + fill_color=mcrfpy.Color(80, 80, 120), + outline=2.0, + outline_color=mcrfpy.Color(120, 120, 180) + ) + label = mcrfpy.Caption(text=text, pos=(60, 18)) + btn.children.append(label) + + def on_enter(pos): + btn.fill_color = mcrfpy.Color(100, 100, 150) + + def on_exit(pos): + btn.fill_color = mcrfpy.Color(80, 80, 120) + + def on_click(pos, button, action): + if action == mcrfpy.InputState.PRESSED: + btn.fill_color = mcrfpy.Color(60, 60, 100) + callback() + elif action == mcrfpy.InputState.RELEASED: + btn.fill_color = mcrfpy.Color(100, 100, 150) + + btn.on_enter = on_enter + btn.on_exit = on_exit + btn.on_click = on_click + return btn + +status = mcrfpy.Caption(text="Click a button", pos=(420, 600)) +scene.children.append(status) + +def click1(): status.text = "Button 1 clicked!" +def click2(): status.text = "Button 2 clicked!" +def click3(): status.text = "Button 3 clicked!" + +scene.children.append(make_button(412, 200, "Button 1", click1)) +scene.children.append(make_button(412, 300, "Button 2", click2)) +scene.children.append(make_button(412, 400, "Button 3", click3)) diff --git a/tests/snippets/073_ui_health_bar.py b/tests/snippets/073_ui_health_bar.py new file mode 100644 index 0000000..8d4290d --- /dev/null +++ b/tests/snippets/073_ui_health_bar.py @@ -0,0 +1,64 @@ +# mcrf: objects=[Caption,Color,Easing,Frame,InputState,Key,Scene] verified=0.2.8-dev@a7ba486 status=ok +# UI Health Bar - Animated health display +import mcrfpy + +scene = mcrfpy.Scene("demo") +mcrfpy.current_scene = scene + +scene.children.append(mcrfpy.Frame(pos=(0, 0), size=(1024, 768), fill_color=mcrfpy.Color(30, 30, 40))) + +class HealthBar: + def __init__(self, x, y, width, height, max_health): + self.max_health = max_health + self.health = max_health + self.width = width + + # Background + self.bg = mcrfpy.Frame( + pos=(x, y), size=(width, height), + fill_color=mcrfpy.Color(60, 20, 20), + outline=2.0, outline_color=mcrfpy.Color(100, 100, 100) + ) + + # Fill bar + self.fill = mcrfpy.Frame( + pos=(2, 2), size=(width - 4, height - 4), + fill_color=mcrfpy.Color(200, 50, 50) + ) + self.bg.children.append(self.fill) + + # Label + self.label = mcrfpy.Caption( + text=f"{max_health}/{max_health}", + pos=(width // 2 - 30, height // 2 - 10) + ) + self.bg.children.append(self.label) + + def set_health(self, value): + self.health = max(0, min(value, self.max_health)) + ratio = self.health / self.max_health + self.fill.animate("w", (self.width - 4) * ratio, 0.3, mcrfpy.Easing.EASE_OUT) + self.label.text = f"{self.health}/{self.max_health}" + + # Color based on health + if ratio > 0.5: + self.fill.fill_color = mcrfpy.Color(100, 200, 100) + elif ratio > 0.25: + self.fill.fill_color = mcrfpy.Color(200, 200, 50) + else: + self.fill.fill_color = mcrfpy.Color(200, 50, 50) + +hp_bar = HealthBar(312, 300, 400, 40, 100) +scene.children.append(hp_bar.bg) + +scene.children.append(mcrfpy.Caption(text="Press 1-5 to set health", pos=(380, 400))) + +def on_key(key, action): + if action != mcrfpy.InputState.PRESSED: return + if key == mcrfpy.Key.NUM_1: hp_bar.set_health(100) + elif key == mcrfpy.Key.NUM_2: hp_bar.set_health(75) + elif key == mcrfpy.Key.NUM_3: hp_bar.set_health(50) + elif key == mcrfpy.Key.NUM_4: hp_bar.set_health(25) + elif key == mcrfpy.Key.NUM_5: hp_bar.set_health(0) + +scene.on_key = on_key diff --git a/tests/snippets/074_ui_tooltip.py b/tests/snippets/074_ui_tooltip.py new file mode 100644 index 0000000..7270ec5 --- /dev/null +++ b/tests/snippets/074_ui_tooltip.py @@ -0,0 +1,52 @@ +# mcrf: objects=[Caption,Color,Frame,Scene,Vector] verified=0.2.8-dev@a7ba486 status=ok +# UI Tooltip - Hover tooltip display +import mcrfpy + +scene = mcrfpy.Scene("demo") +mcrfpy.current_scene = scene + +scene.children.append(mcrfpy.Frame(pos=(0, 0), size=(1024, 768), fill_color=mcrfpy.Color(30, 30, 40))) + +# Tooltip (hidden by default) +tooltip = mcrfpy.Frame( + pos=(0, 0), size=(200, 60), + fill_color=mcrfpy.Color(40, 40, 50), + outline=1.0, outline_color=mcrfpy.Color(150, 150, 150), + visible=False +) +tooltip_text = mcrfpy.Caption(text="", pos=(10, 10)) +tooltip.children.append(tooltip_text) + +def make_item(x, y, name, description): + item = mcrfpy.Frame( + pos=(x, y), size=(150, 150), + fill_color=mcrfpy.Color(80, 80, 120) + ) + label = mcrfpy.Caption(text=name, pos=(30, 60)) + item.children.append(label) + + def on_enter(pos): + tooltip.visible = True + tooltip_text.text = description + tooltip.pos = mcrfpy.Vector(pos.x + 20, pos.y + 20) + + def on_exit(pos): + tooltip.visible = False + + def on_move(pos): + tooltip.pos = mcrfpy.Vector(pos.x + 20, pos.y + 20) + + item.on_enter = on_enter + item.on_exit = on_exit + item.on_move = on_move + return item + +scene.children.append(make_item(200, 300, "Sword", "A sharp blade")) +scene.children.append(make_item(400, 300, "Shield", "Blocks attacks")) +scene.children.append(make_item(600, 300, "Potion", "Heals 50 HP")) +scene.children.append(tooltip) # Add last for z-order + +status = mcrfpy.Caption(text="Hover over items to see tooltips", pos=(350, 550)) +status.outline = 2 +status.outline_color = mcrfpy.Color(0, 0, 0) +scene.children.append(status) diff --git a/tests/snippets/075_ui_message_log.py b/tests/snippets/075_ui_message_log.py new file mode 100644 index 0000000..65fb54f --- /dev/null +++ b/tests/snippets/075_ui_message_log.py @@ -0,0 +1,62 @@ +# mcrf: objects=[Caption,Color,Frame,Scene,Timer] verified=0.2.8-dev@a7ba486 status=ok +# UI Message Log - Scrolling text messages +import mcrfpy + +scene = mcrfpy.Scene("demo") +mcrfpy.current_scene = scene + +scene.children.append(mcrfpy.Frame(pos=(0, 0), size=(1024, 768), fill_color=mcrfpy.Color(30, 30, 40))) + +class MessageLog: + def __init__(self, x, y, width, height, max_messages=8): + self.max_messages = max_messages + self.messages = [] + + self.container = mcrfpy.Frame( + pos=(x, y), size=(width, height), + fill_color=mcrfpy.Color(20, 20, 30), + outline=2.0, outline_color=mcrfpy.Color(80, 80, 100), + clip_children=True + ) + + def add_message(self, text, color): + self.messages.append((text, color)) + if len(self.messages) > self.max_messages: + self.messages.pop(0) + self._refresh() + + def _refresh(self): + # Clear existing + while len(self.container.children) > 0: + self.container.children.remove(self.container.children[0]) + + # Add messages + for i, (text, color) in enumerate(self.messages): + cap = mcrfpy.Caption(text=text, pos=(10, 10 + i * 25)) + cap.fill_color = color + self.container.children.append(cap) + +log = MessageLog(262, 200, 500, 250) +scene.children.append(log.container) + +messages = [ + ("You enter the dungeon.", mcrfpy.Color(200, 200, 200)), + ("A goblin appears!", mcrfpy.Color(255, 100, 100)), + ("You attack the goblin.", mcrfpy.Color(255, 200, 100)), + ("The goblin is defeated!", mcrfpy.Color(100, 255, 100)), + ("You found gold!", mcrfpy.Color(255, 255, 100)), + ("You descend deeper...", mcrfpy.Color(150, 150, 255)), +] + +msg_idx = 0 +def add_msg(timer, runtime): + global msg_idx + if msg_idx < len(messages): + log.add_message(*messages[msg_idx]) + msg_idx += 1 + +timer = mcrfpy.Timer("msg", add_msg, 1000) + +_caption = mcrfpy.Caption(text="Message Log Demo", pos=(420, 100)) +_caption.fill_color = mcrfpy.Color(255, 220, 100) +scene.children.append(_caption) diff --git a/tests/snippets/076_effect_screen_shake.py b/tests/snippets/076_effect_screen_shake.py new file mode 100644 index 0000000..e71edfe --- /dev/null +++ b/tests/snippets/076_effect_screen_shake.py @@ -0,0 +1,55 @@ +# mcrf: objects=[Caption,Color,Frame,InputState,Key,Scene,Timer] verified=0.2.8-dev@a7ba486 status=ok +# Effect Screen Shake - Impact feedback +import mcrfpy +import random + +scene = mcrfpy.Scene("demo") +mcrfpy.current_scene = scene + +# Container for all game elements +game_container = mcrfpy.Frame( + pos=(0, 0), size=(1024, 768), + fill_color=mcrfpy.Color(30, 30, 40) +) +scene.children.append(game_container) + +# Some game content +for i in range(5): + frame = mcrfpy.Frame( + pos=(150 + i * 150, 300), size=(100, 100), + fill_color=mcrfpy.Color(100 + i * 30, 100, 200 - i * 30) + ) + game_container.children.append(frame) + +original_pos = (0, 0) +shake_timer = None + +def do_shake(timer, runtime): + offset_x = random.randint(-10, 10) + offset_y = random.randint(-10, 10) + game_container.x = offset_x + game_container.y = offset_y + +def end_shake(timer, runtime): + global shake_timer + if shake_timer: + shake_timer.stop() + shake_timer = None + game_container.x = original_pos[0] + game_container.y = original_pos[1] + +def trigger_shake(): + global shake_timer + shake_timer = mcrfpy.Timer("shake", do_shake, 16) + mcrfpy.Timer("shake_end", end_shake, 300, once=True) + +game_container.children.append(mcrfpy.Caption( + text="Press SPACE for screen shake", + pos=(350, 550) +)) + +def on_key(key, action): + if key == mcrfpy.Key.SPACE and action == mcrfpy.InputState.PRESSED: + trigger_shake() + +scene.on_key = on_key diff --git a/tests/snippets/077_effect_damage_flash.py b/tests/snippets/077_effect_damage_flash.py new file mode 100644 index 0000000..2ff4884 --- /dev/null +++ b/tests/snippets/077_effect_damage_flash.py @@ -0,0 +1,55 @@ +# mcrf: objects=[Caption,Color,Easing,Frame,InputState,Key,Scene,Sprite,Timer] verified=0.2.8-dev@a7ba486 status=ok +# Effect Damage Flash - Hit feedback on frame +import mcrfpy + +scene = mcrfpy.Scene("demo") +mcrfpy.current_scene = scene + +scene.children.append(mcrfpy.Frame(pos=(0, 0), size=(1024, 768), fill_color=mcrfpy.Color(30, 30, 40))) + +# Character frame (use Frame since Sprite doesn't support color tinting) +character = mcrfpy.Frame( + pos=(400, 250), size=(200, 200), + fill_color=mcrfpy.Color(100, 150, 255) +) +scene.children.append(character) + +# Add sprite inside frame +sprite = mcrfpy.Sprite( + texture=mcrfpy.default_texture, + sprite_index=84, + pos=(50, 50), + scale=6.0 +) +character.children.append(sprite) + +scene.children.append(mcrfpy.Caption( + text="Press SPACE to damage", + pos=(400, 550) +)) + +hp_label = mcrfpy.Caption(text="HP: 100", pos=(450, 500)) +hp_label.fill_color = mcrfpy.Color(100, 255, 100) +scene.children.append(hp_label) + +hp = 100 + +def flash_damage(): + global hp + hp = max(0, hp - 10) + hp_label.text = f"HP: {hp}" + + # Flash white then red then back + character.fill_color = mcrfpy.Color(255, 255, 255) + character.animate("fill_color", (255, 100, 100, 255), 0.1, mcrfpy.Easing.LINEAR) + + def restore(timer, runtime): + character.animate("fill_color", (100, 150, 255, 255), 0.2, mcrfpy.Easing.EASE_OUT) + + mcrfpy.Timer("flash_restore", restore, 150, once=True) + +def on_key(key, action): + if key == mcrfpy.Key.SPACE and action == mcrfpy.InputState.PRESSED: + flash_damage() + +scene.on_key = on_key diff --git a/tests/snippets/078_effect_floating_text.py b/tests/snippets/078_effect_floating_text.py new file mode 100644 index 0000000..70c8097 --- /dev/null +++ b/tests/snippets/078_effect_floating_text.py @@ -0,0 +1,50 @@ +# mcrf: objects=[Caption,Color,Easing,Frame,InputState,Scene,Timer] verified=0.2.8-dev@a7ba486 status=ok +# Effect Floating Text - Damage numbers +import mcrfpy +import random + +scene = mcrfpy.Scene("demo") +mcrfpy.current_scene = scene + +scene.children.append(mcrfpy.Frame(pos=(0, 0), size=(1024, 768), fill_color=mcrfpy.Color(30, 30, 40))) + +# Target +target = mcrfpy.Frame( + pos=(412, 284), size=(200, 200), + fill_color=mcrfpy.Color(100, 80, 80) +) +scene.children.append(target) + +scene.children.append(mcrfpy.Caption(text="Click target to deal damage", pos=(360, 550))) + +text_id = 0 + +def spawn_damage_text(x, y, damage): + global text_id + text_id += 1 + text = mcrfpy.Caption(text=f"-{damage}", pos=(x, y)) + text.fill_color = mcrfpy.Color(255, 100, 100) + text.font_size = 32 + scene.children.append(text) + + # Float up and fade + text.animate("y", y - 100, 1.0, mcrfpy.Easing.EASE_OUT) + text.animate("opacity", 0.0, 1.0, mcrfpy.Easing.EASE_IN) + + # Remove after animation using closure to capture text reference + current_text = text + def remove(timer, runtime): + try: + scene.children.remove(current_text) + except: + pass + + mcrfpy.Timer(f"remove_{text_id}", remove, 1100, once=True) + +def on_click(pos, button, action): + if action == mcrfpy.InputState.PRESSED: + damage = random.randint(10, 99) + # pos is relative to target, add target position + spawn_damage_text(pos.x + 412 + 50, pos.y + 284, damage) + +target.on_click = on_click diff --git a/tests/snippets/079_effect_pulse.py b/tests/snippets/079_effect_pulse.py new file mode 100644 index 0000000..5d5291f --- /dev/null +++ b/tests/snippets/079_effect_pulse.py @@ -0,0 +1,39 @@ +# mcrf: objects=[Caption,Color,Frame,Scene,Timer] verified=0.2.8-dev@a7ba486 status=ok +# Effect Pulse - Pulsing highlight +import mcrfpy +import math + +scene = mcrfpy.Scene("demo") +mcrfpy.current_scene = scene + +scene.children.append(mcrfpy.Frame(pos=(0, 0), size=(1024, 768), fill_color=mcrfpy.Color(30, 30, 40))) + +# Pulsing frame +frame = mcrfpy.Frame( + pos=(362, 284), size=(300, 200), + fill_color=mcrfpy.Color(100, 100, 200), + outline=4.0, + outline_color=mcrfpy.Color(150, 150, 255) +) +scene.children.append(frame) + +label = mcrfpy.Caption(text="Pulsing Effect", pos=(80, 80)) +frame.children.append(label) + +time = 0 + +def pulse(timer, runtime): + global time + time += 0.1 + + # Oscillate opacity + opacity = 0.5 + 0.5 * math.sin(time) + frame.opacity = opacity + + # Oscillate outline + outline = 2 + 4 * abs(math.sin(time)) + frame.outline = outline + +timer = mcrfpy.Timer("pulse", pulse, 50) + +scene.children.append(mcrfpy.Caption(text="Continuous pulse animation", pos=(380, 550))) diff --git a/tests/snippets/080_game_turn_based.py b/tests/snippets/080_game_turn_based.py new file mode 100644 index 0000000..c9c6efe --- /dev/null +++ b/tests/snippets/080_game_turn_based.py @@ -0,0 +1,67 @@ +# mcrf: objects=[Caption,Color,Entity,Grid,InputState,Key,Scene,Timer] verified=0.2.8-dev@a7ba486 status=ok +# Game Turn-Based - Simple turn system +import mcrfpy + +scene = mcrfpy.Scene("demo") +mcrfpy.current_scene = scene + +grid = mcrfpy.Grid( + grid_size=(16, 12), + texture=mcrfpy.default_texture, + pos=(0, 0), + size=(1024, 768), + zoom=4.0 +) +scene.children.append(grid) + +for y in range(12): + for x in range(16): + grid.at(x, y).tilesprite = 48 + grid.at(x, y).walkable = True + +# Player +player = mcrfpy.Entity(grid_pos=(3, 6), texture=mcrfpy.default_texture, sprite_index=84) +grid.entities.append(player) + +# Enemy +enemy = mcrfpy.Entity(grid_pos=(12, 6), texture=mcrfpy.default_texture, sprite_index=112) +grid.entities.append(enemy) + +turn = "player" +status = mcrfpy.Caption(text="Player turn - WASD to move", pos=(320, 720)) +status.outline = 2 +status.outline_color = mcrfpy.Color(0, 0, 0) +scene.children.append(status) + +def enemy_turn(timer, runtime): + global turn + # Simple AI: move toward player + dx = 1 if player.grid_x > enemy.grid_x else -1 if player.grid_x < enemy.grid_x else 0 + dy = 1 if player.grid_y > enemy.grid_y else -1 if player.grid_y < enemy.grid_y else 0 + new_x = enemy.grid_x + dx + new_y = enemy.grid_y + dy + if grid.at(new_x, new_y).walkable: + enemy.grid_pos = (new_x, new_y) + turn = "player" + status.text = "Player turn - WASD to move" + +def on_key(key, action): + global turn + if action != mcrfpy.InputState.PRESSED or turn != "player": + return + + x, y = player.grid_x, player.grid_y + nx, ny = x, y + if key == mcrfpy.Key.W: ny = y - 1 + elif key == mcrfpy.Key.S: ny = y + 1 + elif key == mcrfpy.Key.A: nx = x - 1 + elif key == mcrfpy.Key.D: nx = x + 1 + else: return + + if 0 <= nx < 16 and 0 <= ny < 12 and grid.at(nx, ny).walkable: + player.grid_pos = (nx, ny) + turn = "enemy" + status.text = "Enemy turn..." + mcrfpy.Timer("enemy", enemy_turn, 500, once=True) + +scene.on_key = on_key diff --git a/tests/snippets/081_game_inventory.py b/tests/snippets/081_game_inventory.py new file mode 100644 index 0000000..4b332f5 --- /dev/null +++ b/tests/snippets/081_game_inventory.py @@ -0,0 +1,57 @@ +# mcrf: objects=[Caption,Color,Frame,Scene,Sprite] verified=0.2.8-dev@a7ba486 status=ok +# Game Inventory - Grid-based inventory UI +import mcrfpy + +scene = mcrfpy.Scene("demo") +mcrfpy.current_scene = scene + +scene.children.append(mcrfpy.Frame(pos=(0, 0), size=(1024, 768), fill_color=mcrfpy.Color(30, 30, 40))) + +# Inventory grid +inv_frame = mcrfpy.Frame( + pos=(262, 184), size=(500, 400), + fill_color=mcrfpy.Color(40, 40, 60), + outline=3.0, outline_color=mcrfpy.Color(100, 100, 140) +) +scene.children.append(inv_frame) + +inv_frame.children.append(mcrfpy.Caption(text="Inventory", pos=(200, 20))) + +# Create inventory slots +slots = [] +items = [84, 17, 65, 0, 0, 0, 112, 0, 88, 0, 0, 0] # Sprite indices, 0 = empty + +for i in range(12): + x = 30 + (i % 4) * 115 + y = 70 + (i // 4) * 100 + + slot = mcrfpy.Frame( + pos=(x, y), size=(90, 80), + fill_color=mcrfpy.Color(60, 60, 80), + outline=2.0, outline_color=mcrfpy.Color(80, 80, 100) + ) + inv_frame.children.append(slot) + + if items[i] > 0: + sprite = mcrfpy.Sprite( + texture=mcrfpy.default_texture, + sprite_index=items[i], + pos=(25, 15), + scale=3.0 + ) + slot.children.append(sprite) + + # Hover effect + def make_hover(s): + def enter(pos): s.fill_color = mcrfpy.Color(80, 80, 100) + def exit(pos): s.fill_color = mcrfpy.Color(60, 60, 80) + return enter, exit + + enter, exit = make_hover(slot) + slot.on_enter = enter + slot.on_exit = exit + +status = mcrfpy.Caption(text="Hover over slots to highlight", pos=(400, 620)) +status.outline = 2 +status.outline_color = mcrfpy.Color(0, 0, 0) +scene.children.append(status) diff --git a/tests/snippets/082_game_minimap.py b/tests/snippets/082_game_minimap.py new file mode 100644 index 0000000..2b8cfcc --- /dev/null +++ b/tests/snippets/082_game_minimap.py @@ -0,0 +1,66 @@ +# mcrf: objects=[Caption,Color,Entity,Grid,InputState,Key,Scene] verified=0.2.8-dev@a7ba486 status=ok +# Game Minimap - Small overview map +import mcrfpy + +scene = mcrfpy.Scene("demo") +mcrfpy.current_scene = scene + +# Main game grid - fills most of the screen +main_grid = mcrfpy.Grid( + grid_size=(16, 12), + texture=mcrfpy.default_texture, + pos=(0, 0), + size=(800, 600), + zoom=3.0 +) +scene.children.append(main_grid) + +# Create dungeon +for y in range(12): + for x in range(16): + cell = main_grid.at(x, y) + if x == 0 or x == 15 or y == 0 or y == 11: + cell.tilesprite = 1 + elif x == 8 and y > 2 and y < 9: + cell.tilesprite = 1 + else: + cell.tilesprite = 48 + +player = mcrfpy.Entity(grid_pos=(4, 6), texture=mcrfpy.default_texture, sprite_index=84) +main_grid.entities.append(player) + +# Minimap - small overview in corner +minimap = mcrfpy.Grid( + grid_size=(16, 12), + texture=mcrfpy.default_texture, + pos=(820, 20), + size=(180, 135), + zoom=0.7 +) +scene.children.append(minimap) + +# Copy layout to minimap +for y in range(12): + for x in range(16): + minimap.at(x, y).tilesprite = main_grid.at(x, y).tilesprite + +# Minimap player marker +mini_player = mcrfpy.Entity(grid_pos=(4, 6), texture=mcrfpy.default_texture, sprite_index=84) +minimap.entities.append(mini_player) + +def on_key(key, action): + if action != mcrfpy.InputState.PRESSED: return + x, y = player.grid_x, player.grid_y + if key == mcrfpy.Key.W and main_grid.at(x, y-1).tilesprite == 48: y -= 1 + elif key == mcrfpy.Key.S and main_grid.at(x, y+1).tilesprite == 48: y += 1 + elif key == mcrfpy.Key.A and main_grid.at(x-1, y).tilesprite == 48: x -= 1 + elif key == mcrfpy.Key.D and main_grid.at(x+1, y).tilesprite == 48: x += 1 + player.grid_pos = (x, y) + mini_player.grid_pos = (x, y) + +scene.on_key = on_key + +status = mcrfpy.Caption(text="WASD to move, watch minimap", pos=(320, 720)) +status.outline = 2 +status.outline_color = mcrfpy.Color(0, 0, 0) +scene.children.append(status) diff --git a/tests/snippets/083_grid_layers.py b/tests/snippets/083_grid_layers.py new file mode 100644 index 0000000..9fe60ed --- /dev/null +++ b/tests/snippets/083_grid_layers.py @@ -0,0 +1,43 @@ +# mcrf: objects=[Caption,Color,ColorLayer,Grid,Scene,TileLayer] verified=0.2.8-dev@a7ba486 status=ok +# Grid Layers - Multiple tile layers +import mcrfpy + +scene = mcrfpy.Scene("demo") +mcrfpy.current_scene = scene + +grid = mcrfpy.Grid( + grid_size=(16, 12), + texture=mcrfpy.default_texture, + pos=(0, 0), + size=(1024, 768), + zoom=4.0 +) +scene.children.append(grid) + +# Base layer - floor +for y in range(12): + for x in range(16): + grid.at(x, y).tilesprite = 48 + +# Add a color overlay layer +color_layer = mcrfpy.ColorLayer(z_index=1, name="highlights") +grid.add_layer(color_layer) + +# Highlight some cells with color +for x in range(4, 12): + color_layer.set((x, 5), mcrfpy.Color(100, 200, 255, 100)) + color_layer.set((x, 6), mcrfpy.Color(100, 200, 255, 100)) + +# Add a tile layer for decorations +tile_layer = mcrfpy.TileLayer(z_index=2, name="decorations", texture=mcrfpy.default_texture) +grid.add_layer(tile_layer) + +# Add some decoration tiles +tile_layer.set((5, 3), 17) # Chest +tile_layer.set((10, 3), 65) # Door +tile_layer.set((7, 8), 80) # Water + +status = mcrfpy.Caption(text="Grid with multiple layers: base + color + decoration", pos=(280, 720)) +status.outline = 2 +status.outline_color = mcrfpy.Color(0, 0, 0) +scene.children.append(status) diff --git a/tests/snippets/084_bresenham_line.py b/tests/snippets/084_bresenham_line.py new file mode 100644 index 0000000..c80c887 --- /dev/null +++ b/tests/snippets/084_bresenham_line.py @@ -0,0 +1,46 @@ +# mcrf: objects=[Caption,Color,ColorLayer,Grid,Scene] verified=0.2.8-dev@a7ba486 status=ok +# Bresenham Line - Draw lines on grid +import mcrfpy + +scene = mcrfpy.Scene("demo") +mcrfpy.current_scene = scene + +grid = mcrfpy.Grid( + grid_size=(20, 15), + texture=mcrfpy.default_texture, + pos=(112, 84), + size=(800, 600) +) +scene.children.append(grid) + +# Add color layer for line visualization +line_layer = mcrfpy.ColorLayer(z_index=1, name="line") +grid.add_layer(line_layer) + +# Fill with floor +for y in range(15): + for x in range(20): + grid.at(x, y).tilesprite = 48 + +# Draw lines using bresenham +start = (2, 2) +end = (17, 12) + +# Get all cells along the line +line_cells = mcrfpy.bresenham(start, end) + +# Highlight the line +for x, y in line_cells: + line_layer.set((x, y), mcrfpy.Color(255, 200, 100, 200)) + +# Mark start and end +line_layer.set((start[0], start[1]), mcrfpy.Color(100, 255, 100, 255)) +line_layer.set((end[0], end[1]), mcrfpy.Color(255, 100, 100, 255)) + +status = mcrfpy.Caption( + text=f"Bresenham line: {len(line_cells)} cells - Green=start, Red=end", + pos=(280, 700) +) +status.outline = 2 +status.outline_color = mcrfpy.Color(0, 0, 0) +scene.children.append(status) diff --git a/tests/snippets/085_entity_visibility.py b/tests/snippets/085_entity_visibility.py new file mode 100644 index 0000000..9ebe030 --- /dev/null +++ b/tests/snippets/085_entity_visibility.py @@ -0,0 +1,50 @@ +# mcrf: objects=[Caption,Color,Entity,Grid,Scene,Timer] verified=0.2.8-dev@a7ba486 status=ok +# Entity Visibility - Check what entities can see +import mcrfpy + +scene = mcrfpy.Scene("demo") +mcrfpy.current_scene = scene + +grid = mcrfpy.Grid( + grid_size=(16, 12), + texture=mcrfpy.default_texture, + pos=(0, 0), + size=(1024, 768), + zoom=4.0 +) +scene.children.append(grid) + +# Create dungeon +for y in range(12): + for x in range(16): + cell = grid.at(x, y) + if x == 8 and y > 1 and y < 10: + cell.tilesprite = 1 + cell.transparent = False + else: + cell.tilesprite = 48 + cell.transparent = True + +# Player +player = mcrfpy.Entity(grid_pos=(4, 6), texture=mcrfpy.default_texture, sprite_index=84, name="player") +grid.entities.append(player) + +# Enemies +for i, pos in enumerate([(12, 3), (12, 6), (12, 9)]): + enemy = mcrfpy.Entity(grid_pos=pos, texture=mcrfpy.default_texture, sprite_index=112, name=f"enemy_{i}") + grid.entities.append(enemy) + +grid.perspective = player +grid.fov_radius = 10 + +status = mcrfpy.Caption(text="Wall blocks view of some enemies", pos=(320, 700)) +status.outline = 2 +status.outline_color = mcrfpy.Color(0, 0, 0) +scene.children.append(status) + +def update_status(timer, runtime): + visible = player.visible_entities() + names = [e.name for e in visible if e.name != "player"] + status.text = f"Visible entities: {names if names else 'none'}" + +timer = mcrfpy.Timer("check", update_status, 100) diff --git a/tests/snippets/086_procgen_cellular.py b/tests/snippets/086_procgen_cellular.py new file mode 100644 index 0000000..1c1ac22 --- /dev/null +++ b/tests/snippets/086_procgen_cellular.py @@ -0,0 +1,59 @@ +# mcrf: objects=[Caption,Grid,Scene] verified=0.2.8-dev@a7ba486 status=ok +# Procgen Cellular - Cave generation +import mcrfpy +import random + +scene = mcrfpy.Scene("demo") +mcrfpy.current_scene = scene + +grid = mcrfpy.Grid( + grid_size=(40, 30), + texture=mcrfpy.default_texture, + pos=(112, 84), + size=(800, 600) +) +scene.children.append(grid) + +# Initialize with random walls +for y in range(30): + for x in range(40): + cell = grid.at(x, y) + if x == 0 or x == 39 or y == 0 or y == 29: + cell.tilesprite = 1 + elif random.random() < 0.45: + cell.tilesprite = 1 + else: + cell.tilesprite = 48 + +# Cellular automata iterations +def count_neighbors(x, y): + count = 0 + for dy in range(-1, 2): + for dx in range(-1, 2): + if dx == 0 and dy == 0: + continue + nx, ny = x + dx, y + dy + if 0 <= nx < 40 and 0 <= ny < 30: + if grid.at(nx, ny).tilesprite == 1: + count += 1 + else: + count += 1 + return count + +def iterate(): + changes = [] + for y in range(1, 29): + for x in range(1, 39): + neighbors = count_neighbors(x, y) + if neighbors > 4: + changes.append((x, y, 1)) + elif neighbors < 4: + changes.append((x, y, 48)) + for x, y, tile in changes: + grid.at(x, y).tilesprite = tile + +# Run 4 iterations +for _ in range(4): + iterate() + +scene.children.append(mcrfpy.Caption(text="Cellular automata cave generation", pos=(340, 700))) diff --git a/tests/snippets/087_grid_point_state.py b/tests/snippets/087_grid_point_state.py new file mode 100644 index 0000000..9c13966 --- /dev/null +++ b/tests/snippets/087_grid_point_state.py @@ -0,0 +1,54 @@ +# mcrf: objects=[Caption,Color,ColorLayer,Grid,InputState,Scene] verified=0.2.8-dev@a7ba486 status=ok +# Grid Point State - Cell properties +import mcrfpy + +scene = mcrfpy.Scene("demo") +mcrfpy.current_scene = scene + +grid = mcrfpy.Grid( + grid_size=(16, 12), + texture=mcrfpy.default_texture, + pos=(0, 0), + size=(1024, 768), + zoom=4.0 +) +scene.children.append(grid) + +# Add visual layer for highlighting +highlight_layer = mcrfpy.ColorLayer(z_index=1, name="highlight") +grid.add_layer(highlight_layer) + +# Setup cells with different properties +for y in range(12): + for x in range(16): + cell = grid.at(x, y) + cell.tilesprite = 48 + cell.walkable = True + cell.transparent = True + +# Mark some cells as walls +for i in range(5): + cell = grid.at(8, 3 + i) + cell.tilesprite = 1 + cell.walkable = False + cell.transparent = False + +# Highlight left half as "explored" +for x in range(8): + for y in range(12): + highlight_layer.set((x, y), mcrfpy.Color(100, 200, 100, 80)) + +# Show cell info on click +status = mcrfpy.Caption(text="Click a cell to see properties", pos=(350, 720)) +status.outline = 2 +status.outline_color = mcrfpy.Color(0, 0, 0) +scene.children.append(status) + +def on_cell_click(pos, button, action): + if action == mcrfpy.InputState.PRESSED: + x, y = int(pos.x), int(pos.y) + if 0 <= x < 16 and 0 <= y < 12: + cell = grid.at(x, y) + status.text = f"Cell ({x},{y}): walkable={cell.walkable}, transparent={cell.transparent}" + +grid.on_cell_click = on_cell_click diff --git a/tests/snippets/088_animation_multiple.py b/tests/snippets/088_animation_multiple.py new file mode 100644 index 0000000..19b55ce --- /dev/null +++ b/tests/snippets/088_animation_multiple.py @@ -0,0 +1,49 @@ +# mcrf: objects=[Caption,Color,Easing,Frame,Scene,Timer] verified=0.2.8-dev@a7ba486 status=ok +# Animation Multiple - Several animations at once +import mcrfpy + +scene = mcrfpy.Scene("demo") +mcrfpy.current_scene = scene + +scene.children.append(mcrfpy.Frame(pos=(0, 0), size=(1024, 768), fill_color=mcrfpy.Color(30, 30, 40))) + +# Create a frame +frame = mcrfpy.Frame( + pos=(100, 300), size=(100, 100), + fill_color=mcrfpy.Color(255, 100, 100), + outline=2.0, + outline_color=mcrfpy.Color(255, 255, 255), + opacity=1.0 +) +scene.children.append(frame) + +scene.children.append(mcrfpy.Caption( + text="Multiple animations running simultaneously", + pos=(300, 600) +)) + +def restart_animation(timer, runtime): + frame.x = 100 + frame.y = 300 + frame.w = 100 + frame.h = 100 + frame.fill_color = mcrfpy.Color(255, 100, 100) + frame.opacity = 1.0 + frame.outline = 2.0 + frame.animate("x", 800, 3.0, mcrfpy.Easing.EASE_IN_OUT) + frame.animate("y", 500, 3.0, mcrfpy.Easing.EASE_IN_OUT_BACK) + frame.animate("w", 200, 3.0, mcrfpy.Easing.EASE_OUT_ELASTIC) + frame.animate("h", 150, 3.0, mcrfpy.Easing.EASE_OUT_ELASTIC) + frame.animate("fill_color", (100, 100, 255, 255), 3.0, mcrfpy.Easing.LINEAR) + frame.animate("opacity", 0.5, 3.0, mcrfpy.Easing.EASE_IN) + frame.animate("outline", 10.0, 3.0, mcrfpy.Easing.LINEAR) + +# Start multiple animations at the same time +frame.animate("x", 800, 3.0, mcrfpy.Easing.EASE_IN_OUT) +frame.animate("y", 500, 3.0, mcrfpy.Easing.EASE_IN_OUT_BACK) +frame.animate("w", 200, 3.0, mcrfpy.Easing.EASE_OUT_ELASTIC) +frame.animate("h", 150, 3.0, mcrfpy.Easing.EASE_OUT_ELASTIC) +frame.animate("fill_color", (100, 100, 255, 255), 3.0, mcrfpy.Easing.LINEAR) +frame.animate("opacity", 0.5, 3.0, mcrfpy.Easing.EASE_IN) +frame.animate("outline", 10.0, 3.0, mcrfpy.Easing.LINEAR) +loop_timer = mcrfpy.Timer("loop", restart_animation, 8000) diff --git a/tests/snippets/089_camera_follow.py b/tests/snippets/089_camera_follow.py new file mode 100644 index 0000000..0e67c54 --- /dev/null +++ b/tests/snippets/089_camera_follow.py @@ -0,0 +1,60 @@ +# mcrf: objects=[Caption,Color,Easing,Entity,Grid,InputState,Key,Scene] verified=0.2.8-dev@a7ba486 status=ok +# Camera Follow - Camera tracks entity +import mcrfpy + +scene = mcrfpy.Scene("demo") +mcrfpy.current_scene = scene + +grid = mcrfpy.Grid( + grid_size=(30, 30), + texture=mcrfpy.default_texture, + pos=(0, 0), + size=(1024, 768), + zoom=2.0 +) +scene.children.append(grid) + +# Create large map +for y in range(30): + for x in range(30): + cell = grid.at(x, y) + if x == 0 or x == 29 or y == 0 or y == 29: + cell.tilesprite = 1 + cell.walkable = False + elif (x + y) % 7 == 0 and x > 1 and x < 28 and y > 1 and y < 28: + cell.tilesprite = 1 + cell.walkable = False + else: + cell.tilesprite = 48 + cell.walkable = True + +# Player in center +player = mcrfpy.Entity(grid_pos=(15, 15), texture=mcrfpy.default_texture, sprite_index=84) +grid.entities.append(player) + +# Center camera on player +grid.center_camera((15, 15)) + +def on_key(key, action): + if action != mcrfpy.InputState.PRESSED: + return + x, y = player.grid_x, player.grid_y + nx, ny = x, y + if key == mcrfpy.Key.W: ny -= 1 + elif key == mcrfpy.Key.S: ny += 1 + elif key == mcrfpy.Key.A: nx -= 1 + elif key == mcrfpy.Key.D: nx += 1 + else: return + + if grid.at(nx, ny).walkable: + player.grid_pos = (nx, ny) + # Animate camera to follow (center uses pixel coords: tile * 16 for 16x16 sprites) + grid.animate("center_x", nx * 16 + 8, 0.2, mcrfpy.Easing.EASE_OUT) + grid.animate("center_y", ny * 16 + 8, 0.2, mcrfpy.Easing.EASE_OUT) + +scene.on_key = on_key + +status = mcrfpy.Caption(text="WASD - Camera follows player", pos=(380, 720)) +status.outline = 2 +status.outline_color = mcrfpy.Color(0, 0, 0) +scene.children.append(status) diff --git a/tests/snippets/090_scene_lifecycle.py b/tests/snippets/090_scene_lifecycle.py new file mode 100644 index 0000000..e0e850a --- /dev/null +++ b/tests/snippets/090_scene_lifecycle.py @@ -0,0 +1,45 @@ +# mcrf: objects=[Caption,Color,Frame,InputState,Key,Scene] verified=0.2.8-dev@a7ba486 status=ok +# Scene Lifecycle - on_enter/on_exit callbacks +import mcrfpy + +# Create scenes with lifecycle callbacks +class MenuScene(mcrfpy.Scene): + def __init__(self): + super().__init__("menu") + self.children.append(mcrfpy.Frame(pos=(0, 0), size=(1024, 768), fill_color=mcrfpy.Color(50, 50, 80))) + self.status = mcrfpy.Caption(text="Menu Scene", pos=(420, 300)) + self.status.fill_color = mcrfpy.Color(255, 220, 100) + self.children.append(self.status) + self.children.append(mcrfpy.Caption(text="Press SPACE for game", pos=(380, 400))) + self.on_key = self.handle_key + + def on_enter(self): + self.status.text = "Menu Scene (entered!)" + + def on_exit(self): + print("Leaving menu scene") + + def handle_key(self, key, action): + if key == mcrfpy.Key.SPACE and action == mcrfpy.InputState.PRESSED: + game_scene.activate() + +class GameScene(mcrfpy.Scene): + def __init__(self): + super().__init__("game") + self.children.append(mcrfpy.Frame(pos=(0, 0), size=(1024, 768), fill_color=mcrfpy.Color(80, 50, 50))) + self.status = mcrfpy.Caption(text="Game Scene", pos=(420, 300)) + self.status.fill_color = mcrfpy.Color(100, 255, 100) + self.children.append(self.status) + self.children.append(mcrfpy.Caption(text="Press SPACE for menu", pos=(380, 400))) + self.on_key = self.handle_key + + def on_enter(self): + self.status.text = "Game Scene (entered!)" + + def handle_key(self, key, action): + if key == mcrfpy.Key.SPACE and action == mcrfpy.InputState.PRESSED: + menu_scene.activate() + +menu_scene = MenuScene() +game_scene = GameScene() +mcrfpy.current_scene = menu_scene diff --git a/tests/snippets/091_metrics_display.py b/tests/snippets/091_metrics_display.py new file mode 100644 index 0000000..9fe033d --- /dev/null +++ b/tests/snippets/091_metrics_display.py @@ -0,0 +1,44 @@ +# mcrf: objects=[Caption,Color,Frame,Scene,Timer] verified=0.2.8-dev@a7ba486 status=ok +# Metrics Display - Performance monitoring +import mcrfpy + +scene = mcrfpy.Scene("demo") +mcrfpy.current_scene = scene + +scene.children.append(mcrfpy.Frame(pos=(0, 0), size=(1024, 768), fill_color=mcrfpy.Color(30, 30, 40))) + +_caption = mcrfpy.Caption( + text="Performance Metrics", + pos=(400, 80)) +_caption.fill_color = mcrfpy.Color(255, 220, 100) +scene.children.append(_caption) + +# Create metric displays +labels = {} +metrics_list = ["frame_time", "fps", "draw_calls", "ui_elements", "visible_elements"] + +for i, name in enumerate(metrics_list): + label = mcrfpy.Caption(text=f"{name}: --", pos=(300, 180 + i * 60)) + scene.children.append(label) + labels[name] = label + +# Add some elements to track +for i in range(50): + frame = mcrfpy.Frame( + pos=(100 + (i % 10) * 80, 500 + (i // 10) * 50), + size=(60, 40), + fill_color=mcrfpy.Color(80 + i * 3, 80, 150 - i * 2) + ) + scene.children.append(frame) + +def update_metrics(timer, runtime): + metrics = mcrfpy.getMetrics() + for name in metrics_list: + if name in metrics: + value = metrics[name] + if isinstance(value, float): + labels[name].text = f"{name}: {value:.2f}" + else: + labels[name].text = f"{name}: {value}" + +timer = mcrfpy.Timer("metrics", update_metrics, 100) diff --git a/tests/snippets/092_global_pos.py b/tests/snippets/092_global_pos.py new file mode 100644 index 0000000..f5a73eb --- /dev/null +++ b/tests/snippets/092_global_pos.py @@ -0,0 +1,48 @@ +# mcrf: objects=[Caption,Color,Frame,Scene] verified=0.2.8-dev@a7ba486 status=ok +# Global Position - World coordinates +import mcrfpy + +scene = mcrfpy.Scene("demo") +mcrfpy.current_scene = scene + +scene.children.append(mcrfpy.Frame(pos=(0, 0), size=(1024, 768), fill_color=mcrfpy.Color(30, 30, 40))) + +# Nested containers +outer = mcrfpy.Frame( + pos=(200, 200), size=(500, 400), + fill_color=mcrfpy.Color(60, 60, 80), + outline=2.0 +) +scene.children.append(outer) + +inner = mcrfpy.Frame( + pos=(100, 100), size=(300, 200), + fill_color=mcrfpy.Color(80, 80, 100), + outline=2.0 +) +outer.children.append(inner) + +deepest = mcrfpy.Frame( + pos=(50, 50), size=(100, 80), + fill_color=mcrfpy.Color(150, 100, 100), + outline=2.0 +) +inner.children.append(deepest) + +# Show positions +outer.children.append(mcrfpy.Caption(text="Outer: pos=(200,200)", pos=(10, 10))) +inner.children.append(mcrfpy.Caption(text="Inner: pos=(100,100)", pos=(10, 10))) + +# Global position +gpos = deepest.global_bounds +deepest.children.append(mcrfpy.Caption( + text=f"local=(50,50)", + pos=(5, 5) +)) + +_caption = mcrfpy.Caption( + text=f"Deepest global_pos = ({gpos[0].x}, {gpos[0].y})", + pos=(300, 650)) +_caption.fill_color = mcrfpy.Color(255, 220, 100) +scene.children.append(_caption) +scene.children.append(mcrfpy.Caption(text="(200 + 100 + 50 = 350, 200 + 100 + 50 = 350)", pos=(300, 690))) diff --git a/tests/snippets/093_parent_reference.py b/tests/snippets/093_parent_reference.py new file mode 100644 index 0000000..2c97e8c --- /dev/null +++ b/tests/snippets/093_parent_reference.py @@ -0,0 +1,40 @@ +# mcrf: objects=[Caption,Color,Frame,InputState,Scene] verified=0.2.8-dev@a7ba486 status=ok +# Parent Reference - Access parent from child +import mcrfpy + +scene = mcrfpy.Scene("demo") +mcrfpy.current_scene = scene + +scene.children.append(mcrfpy.Frame(pos=(0, 0), size=(1024, 768), fill_color=mcrfpy.Color(30, 30, 40))) + +# Parent container +parent = mcrfpy.Frame( + pos=(262, 184), size=(500, 400), + fill_color=mcrfpy.Color(80, 80, 120), + name="parent_frame" +) +scene.children.append(parent) + +# Child button +child = mcrfpy.Frame( + pos=(150, 150), size=(200, 100), + fill_color=mcrfpy.Color(100, 150, 100), + name="child_button" +) +parent.children.append(child) + +child.children.append(mcrfpy.Caption(text="Click me!", pos=(50, 35))) + +status = mcrfpy.Caption(text="Click the button", pos=(350, 620)) +scene.children.append(status) + +def on_click(pos, button, action): + if action == mcrfpy.InputState.PRESSED: + # Access parent from child + p = child.parent + if p: + status.text = f"My parent is: '{p.name}'" + # Modify parent + p.fill_color = mcrfpy.Color(120, 80, 80) + +child.on_click = on_click diff --git a/tests/snippets/094_entity_die.py b/tests/snippets/094_entity_die.py new file mode 100644 index 0000000..871463a --- /dev/null +++ b/tests/snippets/094_entity_die.py @@ -0,0 +1,48 @@ +# mcrf: objects=[Caption,Color,Entity,Grid,InputState,Key,Scene] verified=0.2.8-dev@a7ba486 status=ok +# Entity Die - Remove entity from grid +import mcrfpy + +scene = mcrfpy.Scene("demo") +mcrfpy.current_scene = scene + +grid = mcrfpy.Grid( + grid_size=(16, 12), + texture=mcrfpy.default_texture, + pos=(0, 0), + size=(1024, 768), + zoom=4.0 +) +scene.children.append(grid) + +for y in range(12): + for x in range(16): + grid.at(x, y).tilesprite = 48 + +# Create several enemies +enemies = [] +for i in range(5): + enemy = mcrfpy.Entity( + grid_pos=(3 + i * 2, 6), + texture=mcrfpy.default_texture, + sprite_index=112, + name=f"enemy_{i}" + ) + grid.entities.append(enemy) + enemies.append(enemy) + +status = mcrfpy.Caption(text=f"Enemies: {len(grid.entities)} - Press SPACE to kill one", pos=(300, 720)) +status.outline = 2 +status.outline_color = mcrfpy.Color(0, 0, 0) +scene.children.append(status) + +def on_key(key, action): + if key == mcrfpy.Key.SPACE and action == mcrfpy.InputState.PRESSED: + if len(grid.entities) > 0: + # Remove first entity + entity = grid.entities[0] + entity.die() # Removes from grid + status.text = f"Enemies remaining: {len(grid.entities)}" + else: + status.text = "No enemies left!" + +scene.on_key = on_key diff --git a/tests/snippets/095_entity_index.py b/tests/snippets/095_entity_index.py new file mode 100644 index 0000000..f4f2402 --- /dev/null +++ b/tests/snippets/095_entity_index.py @@ -0,0 +1,48 @@ +# mcrf: objects=[Caption,Color,Entity,Grid,InputState,Scene] verified=0.2.8-dev@a7ba486 status=ok +# Entity Index - Get entity position in collection +import mcrfpy + +scene = mcrfpy.Scene("demo") +mcrfpy.current_scene = scene + +grid = mcrfpy.Grid( + grid_size=(16, 12), + texture=mcrfpy.default_texture, + pos=(0, 0), + size=(1024, 768), + zoom=4.0 +) +scene.children.append(grid) + +for y in range(12): + for x in range(16): + grid.at(x, y).tilesprite = 48 + +# Create entities +sprites = [84, 86, 88, 112, 116] +for i, sprite_idx in enumerate(sprites): + entity = mcrfpy.Entity( + grid_pos=(3 + i * 2, 6), + texture=mcrfpy.default_texture, + sprite_index=sprite_idx, + name=f"entity_{i}" + ) + grid.entities.append(entity) + +status = mcrfpy.Caption(text="Click on entities to see their index", pos=(340, 720)) +status.outline = 2 +status.outline_color = mcrfpy.Color(0, 0, 0) +scene.children.append(status) + +def on_cell_click(pos, button, action): + if action == mcrfpy.InputState.PRESSED: + x, y = int(pos.x), int(pos.y) + # Check if entity at position + for entity in grid.entities: + if entity.grid_x == x and entity.grid_y == y: + idx = entity.index() + status.text = f"'{entity.name}' is at index {idx} in grid.entities" + return + status.text = f"No entity at ({x}, {y})" + +grid.on_cell_click = on_cell_click diff --git a/tests/snippets/096_timer_remaining.py b/tests/snippets/096_timer_remaining.py new file mode 100644 index 0000000..056eda3 --- /dev/null +++ b/tests/snippets/096_timer_remaining.py @@ -0,0 +1,53 @@ +# mcrf: objects=[Caption,Color,Frame,Scene,Timer] verified=0.2.8-dev@a7ba486 status=ok +# Timer Remaining - Check time until next tick +import mcrfpy + +scene = mcrfpy.Scene("demo") +mcrfpy.current_scene = scene + +scene.children.append(mcrfpy.Frame(pos=(0, 0), size=(1024, 768), fill_color=mcrfpy.Color(30, 30, 40))) + +_caption = mcrfpy.Caption( + text="Timer Properties", + pos=(420, 100)) +_caption.fill_color = mcrfpy.Color(255, 220, 100) +scene.children.append(_caption) + +# Progress bar background +bar_bg = mcrfpy.Frame( + pos=(212, 300), size=(600, 40), + fill_color=mcrfpy.Color(50, 50, 70), + outline=2.0 +) +scene.children.append(bar_bg) + +# Progress bar fill +bar_fill = mcrfpy.Frame( + pos=(2, 2), size=(596, 36), + fill_color=mcrfpy.Color(100, 200, 100) +) +bar_bg.children.append(bar_fill) + +status = mcrfpy.Caption(text="", pos=(350, 400)) +scene.children.append(status) + +tick_count = 0 +tick_label = mcrfpy.Caption(text="Ticks: 0", pos=(450, 500)) +scene.children.append(tick_label) + +def on_tick(timer, runtime): + global tick_count + tick_count += 1 + tick_label.text = f"Ticks: {tick_count}" + +# 3 second timer +main_timer = mcrfpy.Timer("main", on_tick, 3000) + +def update_display(timer, runtime): + remaining = main_timer.remaining + interval = main_timer.interval + progress = 1.0 - (remaining / interval) + bar_fill.w = int(596 * progress) + status.text = f"Remaining: {remaining}ms / {interval}ms" + +display_timer = mcrfpy.Timer("display", update_display, 16) diff --git a/tests/snippets/097_hovered_state.py b/tests/snippets/097_hovered_state.py new file mode 100644 index 0000000..c404ce4 --- /dev/null +++ b/tests/snippets/097_hovered_state.py @@ -0,0 +1,39 @@ +# mcrf: objects=[Caption,Color,Frame,Scene,Timer] verified=0.2.8-dev@a7ba486 status=ok +# Hovered State - Check if element is hovered +import mcrfpy + +scene = mcrfpy.Scene("demo") +mcrfpy.current_scene = scene + +scene.children.append(mcrfpy.Frame(pos=(0, 0), size=(1024, 768), fill_color=mcrfpy.Color(30, 30, 40))) + +# Create several frames +frames = [] +for i in range(4): + frame = mcrfpy.Frame( + pos=(150 + i * 200, 300), size=(150, 150), + fill_color=mcrfpy.Color(80, 80, 120), + name=f"frame_{i}" + ) + frame.children.append(mcrfpy.Caption(text=f"Frame {i}", pos=(40, 60))) + scene.children.append(frame) + frames.append(frame) + +status = mcrfpy.Caption(text="Hover over frames", pos=(400, 550)) +scene.children.append(status) + +def check_hover(timer, runtime): + hovered_names = [] + for frame in frames: + if frame.hovered: + hovered_names.append(frame.name) + frame.fill_color = mcrfpy.Color(120, 120, 180) + else: + frame.fill_color = mcrfpy.Color(80, 80, 120) + + if hovered_names: + status.text = f"Currently hovered: {', '.join(hovered_names)}" + else: + status.text = "No frames hovered" + +timer = mcrfpy.Timer("hover_check", check_hover, 50) diff --git a/tests/snippets/098_grid_fill_color.py b/tests/snippets/098_grid_fill_color.py new file mode 100644 index 0000000..82ac5fe --- /dev/null +++ b/tests/snippets/098_grid_fill_color.py @@ -0,0 +1,39 @@ +# mcrf: objects=[Caption,Color,Entity,Frame,Grid,Scene] verified=0.2.8-dev@a7ba486 status=ok +# Grid Fill Color - Background color +import mcrfpy + +scene = mcrfpy.Scene("demo") +mcrfpy.current_scene = scene + +scene.children.append(mcrfpy.Frame(pos=(0, 0), size=(1024, 768), fill_color=mcrfpy.Color(30, 30, 40))) + +# Grid with colored background +grid = mcrfpy.Grid( + grid_size=(16, 12), + texture=mcrfpy.default_texture, + pos=(0, 0), + size=(1024, 768), + zoom=4.0, + fill_color=mcrfpy.Color(40, 60, 80) # Blue-ish background +) +scene.children.append(grid) + +# Sparse tiles to show background (only set some cells) +positions = [(3, 3), (7, 5), (12, 3), (5, 8), (10, 8)] +for x, y in positions: + grid.at(x, y).tilesprite = 48 + +# Add some entities +grid.entities.append(mcrfpy.Entity(grid_pos=(5, 4), texture=mcrfpy.default_texture, sprite_index=84)) +grid.entities.append(mcrfpy.Entity(grid_pos=(10, 6), texture=mcrfpy.default_texture, sprite_index=112)) + +status = mcrfpy.Caption(text="Grid fill_color creates background behind tiles", pos=(300, 720)) +status.outline = 2 +status.outline_color = mcrfpy.Color(0, 0, 0) +scene.children.append(status) + +# Show different fill colors +info = mcrfpy.Caption(text="fill_color = (40, 60, 80) - only some cells have tiles", pos=(280, 50)) +info.outline = 2 +info.outline_color = mcrfpy.Color(0, 0, 0) +scene.children.append(info) diff --git a/tests/snippets/099_scene_pos.py b/tests/snippets/099_scene_pos.py new file mode 100644 index 0000000..a90258b --- /dev/null +++ b/tests/snippets/099_scene_pos.py @@ -0,0 +1,40 @@ +# mcrf: objects=[Caption,Color,Frame,InputState,Key,Scene,Vector] verified=0.2.8-dev@a7ba486 status=ok +# Scene Position - Offset all scene content +import mcrfpy + +scene = mcrfpy.Scene("demo") +mcrfpy.current_scene = scene + +scene.children.append(mcrfpy.Frame(pos=(0, 0), size=(1024, 768), fill_color=mcrfpy.Color(30, 30, 40))) + +# Add some content +for i in range(5): + frame = mcrfpy.Frame( + pos=(150 + i * 150, 300), size=(100, 100), + fill_color=mcrfpy.Color(100 + i * 30, 100, 200 - i * 30) + ) + scene.children.append(frame) + +scene.children.append(mcrfpy.Caption( + text="Press WASD to move scene.pos", + pos=(350, 550) +)) + +pos_label = mcrfpy.Caption(text="scene.pos = (0, 0)", pos=(380, 600)) +scene.children.append(pos_label) + +def on_key(key, action): + if action != mcrfpy.InputState.PRESSED: + return + x, y = scene.pos.x, scene.pos.y + step = 20 + if key == mcrfpy.Key.W: y -= step + elif key == mcrfpy.Key.S: y += step + elif key == mcrfpy.Key.A: x -= step + elif key == mcrfpy.Key.D: x += step + elif key == mcrfpy.Key.R: x, y = 0, 0 # Reset + + scene.pos = mcrfpy.Vector(x, y) + pos_label.text = f"scene.pos = ({x}, {y})" + +scene.on_key = on_key diff --git a/tests/snippets/100_complete_game.py b/tests/snippets/100_complete_game.py new file mode 100644 index 0000000..314b4f2 --- /dev/null +++ b/tests/snippets/100_complete_game.py @@ -0,0 +1,76 @@ +# mcrf: objects=[Caption,Color,Entity,Frame,Grid,InputState,Key,Scene] verified=0.2.8-dev@a7ba486 status=ok +# Complete Mini-Game - Putting it all together +import mcrfpy +import random + +scene = mcrfpy.Scene("game") +mcrfpy.current_scene = scene + +# Game state +score = 0 +game_over = False + +# UI +scene.children.append(mcrfpy.Frame(pos=(0, 0), size=(1024, 768), fill_color=mcrfpy.Color(20, 20, 30))) + +grid = mcrfpy.Grid(grid_size=(16, 12), texture=mcrfpy.default_texture, pos=(0, 0), size=(1024, 768), zoom=4.0) +scene.children.append(grid) + +for y in range(12): + for x in range(16): + cell = grid.at(x, y) + cell.tilesprite = 48 if (x > 0 and x < 15 and y > 0 and y < 11) else 1 + cell.walkable = cell.tilesprite == 48 + +# Player +player = mcrfpy.Entity(grid_pos=(8, 6), texture=mcrfpy.default_texture, sprite_index=84) +grid.entities.append(player) + +# Coins +def spawn_coin(): + while True: + x, y = random.randint(1, 14), random.randint(1, 10) + if grid.at(x, y).walkable: + occupied = False + for e in grid.entities: + if e.grid_x == x and e.grid_y == y: + occupied = True + break + if not occupied: + coin = mcrfpy.Entity(grid_pos=(x, y), texture=mcrfpy.default_texture, sprite_index=17, name="coin") + grid.entities.append(coin) + return + +for _ in range(5): + spawn_coin() + +score_label = mcrfpy.Caption(text="Score: 0", pos=(20, 20)) +score_label.fill_color = mcrfpy.Color(255, 220, 100) +score_label.outline = 2 +score_label.outline_color = mcrfpy.Color(0, 0, 0) +scene.children.append(score_label) + +status = mcrfpy.Caption(text="WASD to collect coins!", pos=(400, 720)) +status.outline = 2 +status.outline_color = mcrfpy.Color(0, 0, 0) +scene.children.append(status) + +def check_collision(): + global score + for entity in list(grid.entities): + if entity.name == "coin" and entity.grid_x == player.grid_x and entity.grid_y == player.grid_y: + entity.die() + score += 10 + score_label.text = f"Score: {score}" + spawn_coin() + +def on_key(key, action): + if action != mcrfpy.InputState.PRESSED: return + x, y = player.grid_x, player.grid_y + if key == mcrfpy.Key.W and grid.at(x, y-1).walkable: player.grid_pos = (x, y-1) + elif key == mcrfpy.Key.S and grid.at(x, y+1).walkable: player.grid_pos = (x, y+1) + elif key == mcrfpy.Key.A and grid.at(x-1, y).walkable: player.grid_pos = (x-1, y) + elif key == mcrfpy.Key.D and grid.at(x+1, y).walkable: player.grid_pos = (x+1, y) + check_collision() + +scene.on_key = on_key diff --git a/tests/snippets/101_grid_camera_rotation.py b/tests/snippets/101_grid_camera_rotation.py new file mode 100644 index 0000000..4357286 --- /dev/null +++ b/tests/snippets/101_grid_camera_rotation.py @@ -0,0 +1,46 @@ +# mcrf: objects=[Caption,Color,Entity,Grid,InputState,Key,Scene] verified=0.2.8-dev@a7ba486 status=ok +# Grid Camera Rotation - Rotate the view +import mcrfpy + +scene = mcrfpy.Scene("demo") +mcrfpy.current_scene = scene + +grid = mcrfpy.Grid( + grid_size=(16, 12), + texture=mcrfpy.default_texture, + pos=(0, 0), + size=(1024, 768), + zoom=4.0 +) +scene.children.append(grid) + +for y in range(12): + for x in range(16): + cell = grid.at(x, y) + cell.tilesprite = 48 if (x + y) % 2 == 0 else 49 + +player = mcrfpy.Entity(grid_pos=(8, 6), texture=mcrfpy.default_texture, sprite_index=84) +grid.entities.append(player) + +grid.center_camera((8, 6)) + +angle_label = mcrfpy.Caption(text="Rotation: 0°", pos=(420, 660)) +angle_label.outline = 2 +angle_label.outline_color = mcrfpy.Color(0, 0, 0) +scene.children.append(angle_label) + +controls = mcrfpy.Caption(text="Q/E to rotate camera", pos=(400, 720)) +controls.outline = 2 +controls.outline_color = mcrfpy.Color(0, 0, 0) +scene.children.append(controls) + +def on_key(key, action): + if action != mcrfpy.InputState.PRESSED: + return + if key == mcrfpy.Key.Q: + grid.camera_rotation -= 15 + elif key == mcrfpy.Key.E: + grid.camera_rotation += 15 + angle_label.text = f"Rotation: {grid.camera_rotation}°" + +scene.on_key = on_key diff --git a/tests/snippets/102_bsp_traversal.py b/tests/snippets/102_bsp_traversal.py new file mode 100644 index 0000000..604c5a8 --- /dev/null +++ b/tests/snippets/102_bsp_traversal.py @@ -0,0 +1,65 @@ +# mcrf: objects=[BSP,Caption,Color,ColorLayer,Frame,Grid,Scene,Traversal] verified=0.2.8-dev@a7ba486 status=ok +# BSP Traversal - Different traversal orders +import mcrfpy + +scene = mcrfpy.Scene("demo") +mcrfpy.current_scene = scene + +scene.children.append(mcrfpy.Frame(pos=(0, 0), size=(1024, 768), fill_color=mcrfpy.Color(30, 30, 40))) + +# Create BSP tree +bsp = mcrfpy.BSP(pos=(0, 0), size=(16, 12)) +bsp.split_recursive(depth=3, min_size=(3, 3)) + +# Visualize in a grid +grid = mcrfpy.Grid( + grid_size=(16, 12), + texture=mcrfpy.default_texture, + pos=(0, 0), + size=(1024, 768), + zoom=4.0 +) +scene.children.append(grid) + +# Add color layer for visualization +color_layer = mcrfpy.ColorLayer(z_index=1, name="rooms") +grid.add_layer(color_layer) + +for y in range(12): + for x in range(16): + grid.at(x, y).tilesprite = 48 + +# Color leaves by traversal order +colors = [ + mcrfpy.Color(255, 100, 100, 150), + mcrfpy.Color(255, 200, 100, 150), + mcrfpy.Color(200, 255, 100, 150), + mcrfpy.Color(100, 255, 100, 150), + mcrfpy.Color(100, 255, 200, 150), + mcrfpy.Color(100, 200, 255, 150), + mcrfpy.Color(100, 100, 255, 150), + mcrfpy.Color(200, 100, 255, 150), +] + +traversal = mcrfpy.Traversal.IN_ORDER +leaves = list(bsp.traverse(traversal)) + +leaf_count = 0 +for i, node in enumerate(leaves): + if node.is_leaf: + x, y = node.pos + w, h = node.size + color = colors[leaf_count % len(colors)] + for ny in range(y, y + h): + for nx in range(x, x + w): + if 0 <= nx < 16 and 0 <= ny < 12: + color_layer.set((nx, ny), color) + leaf_count += 1 + +status = mcrfpy.Caption( + text=f"BSP IN_ORDER traversal - {len(leaves)} nodes, {leaf_count} leaves", + pos=(280, 720) +) +status.outline = 2 +status.outline_color = mcrfpy.Color(0, 0, 0) +scene.children.append(status) diff --git a/tests/snippets/103_animation_pos_vector.py b/tests/snippets/103_animation_pos_vector.py new file mode 100644 index 0000000..f18445a --- /dev/null +++ b/tests/snippets/103_animation_pos_vector.py @@ -0,0 +1,43 @@ +# mcrf: objects=[Caption,Color,Easing,Frame,Scene,Timer] verified=0.2.8-dev@a7ba486 status=ok +# Animation Position - Animate x and y along a path +import mcrfpy + +scene = mcrfpy.Scene("demo") +mcrfpy.current_scene = scene + +scene.children.append(mcrfpy.Frame(pos=(0, 0), size=(1024, 768), fill_color=mcrfpy.Color(30, 30, 40))) + +frame = mcrfpy.Frame( + pos=(100, 100), size=(100, 100), + fill_color=mcrfpy.Color(255, 150, 100) +) +scene.children.append(frame) + +# Path markers +path = [(100, 100), (800, 100), (800, 600), (100, 600)] +for x, y in path: + marker = mcrfpy.Frame( + pos=(x - 5, y - 5), size=(10, 10), + fill_color=mcrfpy.Color(100, 100, 100) + ) + scene.children.append(marker) + +scene.children.append(mcrfpy.Caption(text="Animating x and y position along path", pos=(340, 350))) + +current_idx = 0 + +def move_to_next(timer, runtime): + global current_idx + current_idx = (current_idx + 1) % len(path) + next_pos = path[current_idx] + # Animate both x and y + frame.animate("x", next_pos[0], 1.0, mcrfpy.Easing.EASE_IN_OUT) + frame.animate("y", next_pos[1], 1.0, mcrfpy.Easing.EASE_IN_OUT) + +# Use timer to control path movement (avoids recursive callback segfault) +path_timer = mcrfpy.Timer("path", move_to_next, 1500) + +# Start first animation +frame.animate("x", path[1][0], 1.0, mcrfpy.Easing.EASE_IN_OUT) +frame.animate("y", path[1][1], 1.0, mcrfpy.Easing.EASE_IN_OUT) +current_idx = 1 diff --git a/tests/snippets/104_texture_properties.py b/tests/snippets/104_texture_properties.py new file mode 100644 index 0000000..68de4c7 --- /dev/null +++ b/tests/snippets/104_texture_properties.py @@ -0,0 +1,49 @@ +# mcrf: objects=[Caption,Color,Frame,Scene,Sprite] verified=0.2.8-dev@a7ba486 status=ok +# Texture Properties - Texture information +import mcrfpy + +scene = mcrfpy.Scene("demo") +mcrfpy.current_scene = scene + +scene.children.append(mcrfpy.Frame(pos=(0, 0), size=(1024, 768), fill_color=mcrfpy.Color(30, 30, 40))) + +# Get default texture info +tex = mcrfpy.default_texture + +_caption = mcrfpy.Caption( + text="Default Texture Properties", + pos=(350, 100)) +_caption.fill_color = mcrfpy.Color(255, 220, 100) +scene.children.append(_caption) + +info = [ + f"Source: {tex.source}", + f"Sprite Width: {tex.sprite_width}", + f"Sprite Height: {tex.sprite_height}", + f"Sprite Count: {tex.sprite_count}", + f"Sheet Width: {tex.sheet_width}", + f"Sheet Height: {tex.sheet_height}", +] + +y = 200 +for line in info: + cap = mcrfpy.Caption(text=line, pos=(200, y)) + scene.children.append(cap) + y += 50 + +# Show sample sprites +scene.children.append(mcrfpy.Caption(text="Sample sprites:", pos=(200, 500))) + +for i in range(8): + sprite = mcrfpy.Sprite( + texture=tex, + sprite_index=i * 10, + pos=(200 + i * 90, 550), + scale=4.0 + ) + scene.children.append(sprite) + _caption = mcrfpy.Caption( + text=str(i * 10), + pos=(220 + i * 90, 630)) + _caption.fill_color = mcrfpy.Color(150, 150, 150) + scene.children.append(_caption) diff --git a/tests/snippets/105_scenes_dict.py b/tests/snippets/105_scenes_dict.py new file mode 100644 index 0000000..118344e --- /dev/null +++ b/tests/snippets/105_scenes_dict.py @@ -0,0 +1,35 @@ +# mcrf: objects=[Caption,Color,Frame,Scene] verified=0.2.8-dev@a7ba486 status=ok +# Scenes Tuple - Access all scenes +import mcrfpy + +# Create several scenes +for name in ["menu", "game", "options", "credits"]: + s = mcrfpy.Scene(name) + s.children.append(mcrfpy.Frame(pos=(0, 0), size=(1024, 768), fill_color=mcrfpy.Color(30, 30, 40))) + s.children.append(mcrfpy.Caption(text=f"{name.upper()} SCENE", pos=(420, 350))) + +scene = mcrfpy.Scene("main") +mcrfpy.current_scene = scene + +scene.children.append(mcrfpy.Frame(pos=(0, 0), size=(1024, 768), fill_color=mcrfpy.Color(30, 30, 40))) + +_caption = mcrfpy.Caption( + text="All Registered Scenes", + pos=(380, 100)) +_caption.fill_color = mcrfpy.Color(255, 220, 100) +scene.children.append(_caption) + +# List all scenes (scenes is a tuple) +y = 200 +for s in mcrfpy.scenes: + cap = mcrfpy.Caption( + text=f"• {s.name}", + pos=(300, y) + ) + scene.children.append(cap) + y += 50 + +scene.children.append(mcrfpy.Caption( + text=f"Total scenes: {len(mcrfpy.scenes)}", + pos=(380, 550) +)) diff --git a/tests/snippets/106_timers_list.py b/tests/snippets/106_timers_list.py new file mode 100644 index 0000000..c98f206 --- /dev/null +++ b/tests/snippets/106_timers_list.py @@ -0,0 +1,45 @@ +# mcrf: objects=[Caption,Color,Frame,Scene,Timer] verified=0.2.8-dev@a7ba486 status=ok +# Timers List - View all active timers +import mcrfpy + +scene = mcrfpy.Scene("demo") +mcrfpy.current_scene = scene + +scene.children.append(mcrfpy.Frame(pos=(0, 0), size=(1024, 768), fill_color=mcrfpy.Color(30, 30, 40))) + +_caption = mcrfpy.Caption( + text="Active Timers", + pos=(430, 80)) +_caption.fill_color = mcrfpy.Color(255, 220, 100) +scene.children.append(_caption) + +# Create several timers +def tick1(t, r): pass +def tick2(t, r): pass +def tick3(t, r): pass + +timer1 = mcrfpy.Timer("fast_timer", tick1, 100) +timer2 = mcrfpy.Timer("slow_timer", tick2, 2000) +timer3 = mcrfpy.Timer("paused_timer", tick3, 500) +timer3.pause() + +# Display +timer_display = mcrfpy.Frame( + pos=(162, 150), size=(700, 400), + fill_color=mcrfpy.Color(40, 40, 60) +) +scene.children.append(timer_display) + +def update_display(timer, runtime): + # Clear existing + while len(timer_display.children) > 0: + timer_display.children.remove(timer_display.children[0]) + + y = 20 + for t in mcrfpy.timers: + info = f"{t.name}: interval={t.interval}ms, remaining={t.remaining}ms, paused={t.paused}" + cap = mcrfpy.Caption(text=info, pos=(20, y)) + timer_display.children.append(cap) + y += 50 + +display_timer = mcrfpy.Timer("display", update_display, 100) diff --git a/tests/snippets/107_animations_list.py b/tests/snippets/107_animations_list.py new file mode 100644 index 0000000..aa4756e --- /dev/null +++ b/tests/snippets/107_animations_list.py @@ -0,0 +1,56 @@ +# mcrf: objects=[Caption,Color,Easing,Frame,Scene,Timer] verified=0.2.8-dev@a7ba486 status=ok +# Animations List - View all active animations +import mcrfpy + +scene = mcrfpy.Scene("demo") +mcrfpy.current_scene = scene + +scene.children.append(mcrfpy.Frame(pos=(0, 0), size=(1024, 768), fill_color=mcrfpy.Color(30, 30, 40))) + +_caption = mcrfpy.Caption( + text="Active Animations", + pos=(400, 80)) +_caption.fill_color = mcrfpy.Color(255, 220, 100) +scene.children.append(_caption) + +# Create frames with animations +frames = [] +for i in range(3): + frame = mcrfpy.Frame( + pos=(150 + i * 250, 200), size=(100, 100), + fill_color=mcrfpy.Color(100 + i * 50, 100, 200 - i * 50) + ) + scene.children.append(frame) + frames.append(frame) + +# Start various animations +frames[0].animate("x", 150, 5.0, mcrfpy.Easing.EASE_IN_OUT) +frames[1].animate("opacity", 0.2, 4.0, mcrfpy.Easing.LINEAR) +frames[2].animate("fill_color", (255, 100, 100, 255), 3.0, mcrfpy.Easing.EASE_OUT) + +# Display panel +anim_display = mcrfpy.Frame( + pos=(162, 400), size=(700, 250), + fill_color=mcrfpy.Color(40, 40, 60) +) +scene.children.append(anim_display) + +def update_display(timer, runtime): + # Clear previous + while len(anim_display.children) > 0: + anim_display.children.remove(anim_display.children[0]) + + y = 20 + anim_display.children.append(mcrfpy.Caption( + text=f"Total animations: {len(mcrfpy.animations)}", + pos=(20, y) + )) + y += 40 + + for anim in mcrfpy.animations: + info = f"property='{anim.property}', elapsed={anim.elapsed:.1f}s/{anim.duration:.1f}s" + cap = mcrfpy.Caption(text=info, pos=(20, y)) + anim_display.children.append(cap) + y += 35 + +display_timer = mcrfpy.Timer("display", update_display, 100) diff --git a/tests/snippets/108_cache_subtree.py b/tests/snippets/108_cache_subtree.py new file mode 100644 index 0000000..fa025a0 --- /dev/null +++ b/tests/snippets/108_cache_subtree.py @@ -0,0 +1,54 @@ +# mcrf: objects=[Caption,Color,Frame,Scene] verified=0.2.8-dev@a7ba486 status=ok +# Cache Subtree - Performance optimization +import mcrfpy + +scene = mcrfpy.Scene("demo") +mcrfpy.current_scene = scene + +scene.children.append(mcrfpy.Frame(pos=(0, 0), size=(1024, 768), fill_color=mcrfpy.Color(30, 30, 40))) + +_caption = mcrfpy.Caption( + text="cache_subtree Performance", + pos=(360, 50)) +_caption.fill_color = mcrfpy.Color(255, 220, 100) +scene.children.append(_caption) + +# Complex UI without caching +no_cache = mcrfpy.Frame( + pos=(100, 150), size=(350, 300), + fill_color=mcrfpy.Color(60, 60, 80), + cache_subtree=False +) +scene.children.append(no_cache) +no_cache.children.append(mcrfpy.Caption(text="cache_subtree=False", pos=(80, 10))) + +# Add many children +for i in range(50): + f = mcrfpy.Frame( + pos=(10 + (i % 10) * 33, 40 + (i // 10) * 50), + size=(28, 40), + fill_color=mcrfpy.Color(100 + i * 2, 100, 200 - i * 2) + ) + no_cache.children.append(f) + +# Complex UI with caching +with_cache = mcrfpy.Frame( + pos=(500, 150), size=(350, 300), + fill_color=mcrfpy.Color(60, 60, 80), + cache_subtree=True # Renders children to texture +) +scene.children.append(with_cache) +with_cache.children.append(mcrfpy.Caption(text="cache_subtree=True", pos=(80, 10))) + +for i in range(50): + f = mcrfpy.Frame( + pos=(10 + (i % 10) * 33, 40 + (i // 10) * 50), + size=(28, 40), + fill_color=mcrfpy.Color(100 + i * 2, 100, 200 - i * 2) + ) + with_cache.children.append(f) + +scene.children.append(mcrfpy.Caption( + text="Caching reduces draw calls for static content", + pos=(280, 500) +)) diff --git a/tests/snippets/109_realign.py b/tests/snippets/109_realign.py new file mode 100644 index 0000000..3399157 --- /dev/null +++ b/tests/snippets/109_realign.py @@ -0,0 +1,48 @@ +# mcrf: objects=[Alignment,Caption,Color,Frame,InputState,Key,Scene] verified=0.2.8-dev@a7ba486 status=ok +# Realign - Reapply alignment after changes +import mcrfpy + +scene = mcrfpy.Scene("demo") +mcrfpy.current_scene = scene + +scene.children.append(mcrfpy.Frame(pos=(0, 0), size=(1024, 768), fill_color=mcrfpy.Color(30, 30, 40))) + +# Container +container = mcrfpy.Frame( + pos=(212, 184), size=(600, 400), + fill_color=mcrfpy.Color(50, 50, 70), + outline=2.0 +) +scene.children.append(container) + +# Centered child +child = mcrfpy.Frame( + size=(200, 100), + fill_color=mcrfpy.Color(150, 100, 100), + align=mcrfpy.Alignment.CENTER +) +container.children.append(child) + +child.children.append(mcrfpy.Caption(text="Centered", pos=(60, 35))) + +status = mcrfpy.Caption(text="Press SPACE to resize container", pos=(320, 620)) +scene.children.append(status) + +small = True + +def on_key(key, action): + global small + if key == mcrfpy.Key.SPACE and action == mcrfpy.InputState.PRESSED: + if small: + container.w = 800 + container.h = 500 + status.text = "Container resized! Child realigns automatically" + else: + container.w = 600 + container.h = 400 + status.text = "Container shrunk! Alignment maintained" + small = not small + # realign() is called automatically when parent resizes + # but you can call it manually: child.realign() + +scene.on_key = on_key diff --git a/tests/snippets/110_version_info.py b/tests/snippets/110_version_info.py new file mode 100644 index 0000000..7e51848 --- /dev/null +++ b/tests/snippets/110_version_info.py @@ -0,0 +1,44 @@ +# mcrf: objects=[Caption,Color,Frame,Scene,Sprite] verified=0.2.8-dev@a7ba486 status=ok +# Version Info - Engine version and info +import mcrfpy + +scene = mcrfpy.Scene("demo") +mcrfpy.current_scene = scene + +scene.children.append(mcrfpy.Frame(pos=(0, 0), size=(1024, 768), fill_color=mcrfpy.Color(30, 30, 40))) + +_caption = mcrfpy.Caption( + text="McRogueFace Engine Info", + pos=(360, 100)) +_caption.fill_color = mcrfpy.Color(255, 220, 100) +scene.children.append(_caption) + +# Large logo/sprite +sprite = mcrfpy.Sprite( + texture=mcrfpy.default_texture, + sprite_index=84, + pos=(412, 200), + scale=12.0 +) +scene.children.append(sprite) + +# Version info +info_frame = mcrfpy.Frame( + pos=(262, 420), size=(500, 200), + fill_color=mcrfpy.Color(40, 40, 60), + outline=2.0 +) +scene.children.append(info_frame) + +version = mcrfpy.__version__ +info_frame.children.append(mcrfpy.Caption(text=f"Version: {version}", pos=(150, 30))) +info_frame.children.append(mcrfpy.Caption(text="A Python-powered roguelike engine", pos=(80, 80))) +info_frame.children.append(mcrfpy.Caption(text="Built with SFML, libtcod, and Python", pos=(70, 120))) + +link = mcrfpy.Caption( + text="https://github.com/jmcb/McRogueFace", + pos=(330, 650)) +link.fill_color = mcrfpy.Color(100, 150, 255) +link.outline = 2 +link.outline_color = mcrfpy.Color(0, 0, 0) +scene.children.append(link) diff --git a/tests/snippets/111_alignment_margins.py b/tests/snippets/111_alignment_margins.py new file mode 100644 index 0000000..acfb336 --- /dev/null +++ b/tests/snippets/111_alignment_margins.py @@ -0,0 +1,43 @@ +# mcrf: objects=[Alignment,Caption,Color,Frame,Scene] verified=0.2.8-dev@a7ba486 status=ok +# Alignment Margins - Spacing from edges +import mcrfpy + +scene = mcrfpy.Scene("demo") +mcrfpy.current_scene = scene + +container = mcrfpy.Frame( + pos=(112, 84), size=(800, 600), + fill_color=mcrfpy.Color(40, 40, 60), + outline=2.0 +) +scene.children.append(container) + +# Different margin examples +corners = [ + (mcrfpy.Alignment.TOP_LEFT, 0, "margin=0"), + (mcrfpy.Alignment.TOP_RIGHT, 20, "margin=20"), + (mcrfpy.Alignment.BOTTOM_LEFT, 40, "margin=40"), + (mcrfpy.Alignment.BOTTOM_RIGHT, 60, "margin=60"), +] + +for align, margin, label in corners: + box = mcrfpy.Frame( + size=(120, 80), + fill_color=mcrfpy.Color(150, 100, 100), + align=align, + margin=margin + ) + container.children.append(box) + box.children.append(mcrfpy.Caption(text=label, pos=(15, 30))) + +# Center with margins +center = mcrfpy.Frame( + size=(200, 100), + fill_color=mcrfpy.Color(100, 150, 100), + align=mcrfpy.Alignment.CENTER, + margin=0 +) +container.children.append(center) +center.children.append(mcrfpy.Caption(text="CENTER, no margin", pos=(20, 35))) + +scene.children.append(mcrfpy.Caption(text="Margin affects distance from edges when aligned", pos=(280, 700))) diff --git a/tests/snippets/112_animation_delta.py b/tests/snippets/112_animation_delta.py new file mode 100644 index 0000000..a6aebd9 --- /dev/null +++ b/tests/snippets/112_animation_delta.py @@ -0,0 +1,44 @@ +# mcrf: objects=[Caption,Color,Easing,Frame,InputState,Key,Scene] verified=0.2.8-dev@a7ba486 status=ok +# Animation Delta - Relative animation +import mcrfpy + +scene = mcrfpy.Scene("demo") +mcrfpy.current_scene = scene + +scene.children.append(mcrfpy.Frame(pos=(0, 0), size=(1024, 768), fill_color=mcrfpy.Color(30, 30, 40))) + +_caption = mcrfpy.Caption( + text="Delta Animation (Relative Values)", + pos=(340, 80)) +_caption.fill_color = mcrfpy.Color(255, 220, 100) +scene.children.append(_caption) + +# Frame for absolute animation +abs_frame = mcrfpy.Frame( + pos=(200, 300), size=(100, 100), + fill_color=mcrfpy.Color(255, 100, 100) +) +scene.children.append(abs_frame) +scene.children.append(mcrfpy.Caption(text="Absolute: x -> 500", pos=(200, 420))) + +# Frame for delta animation +delta_frame = mcrfpy.Frame( + pos=(200, 500), size=(100, 100), + fill_color=mcrfpy.Color(100, 100, 255) +) +scene.children.append(delta_frame) +scene.children.append(mcrfpy.Caption(text="Delta: x += 300", pos=(200, 620))) + +scene.children.append(mcrfpy.Caption(text="Press SPACE to animate", pos=(400, 700))) + +def on_key(key, action): + if key == mcrfpy.Key.SPACE and action == mcrfpy.InputState.PRESSED: + # Absolute: animate TO position 500 + abs_frame.animate("x", 500, 1.0, mcrfpy.Easing.EASE_OUT) + + # Delta: animate BY 300 (relative to current position) + # Note: delta animations add to current value + anim = mcrfpy.Animation("x", 300, 1.0, mcrfpy.Easing.EASE_OUT, delta=True) + anim.start(delta_frame) + +scene.on_key = on_key diff --git a/tests/snippets/113_easing_cubic.py b/tests/snippets/113_easing_cubic.py new file mode 100644 index 0000000..2760134 --- /dev/null +++ b/tests/snippets/113_easing_cubic.py @@ -0,0 +1,50 @@ +# mcrf: objects=[Caption,Color,Easing,Frame,Scene,Timer] verified=0.2.8-dev@a7ba486 status=ok +# Easing Cubic - Cubic easing functions +import mcrfpy + +scene = mcrfpy.Scene("demo") +mcrfpy.current_scene = scene + +scene.children.append(mcrfpy.Frame(pos=(0, 0), size=(1024, 768), fill_color=mcrfpy.Color(30, 30, 40))) + +_caption = mcrfpy.Caption( + text="Cubic Easing Functions", + pos=(380, 50)) +_caption.fill_color = mcrfpy.Color(255, 220, 100) +scene.children.append(_caption) + +easings = [ + (mcrfpy.Easing.EASE_IN_CUBIC, "EASE_IN_CUBIC", mcrfpy.Color(255, 100, 100)), + (mcrfpy.Easing.EASE_OUT_CUBIC, "EASE_OUT_CUBIC", mcrfpy.Color(100, 255, 100)), + (mcrfpy.Easing.EASE_IN_OUT_CUBIC, "EASE_IN_OUT_CUBIC", mcrfpy.Color(100, 100, 255)), +] + +frames = [] +for i, (easing, name, color) in enumerate(easings): + y = 150 + i * 180 + + label = mcrfpy.Caption(text=name, pos=(100, y + 20)) + label.fill_color = mcrfpy.Color(180, 180, 180) + scene.children.append(label) + + frame = mcrfpy.Frame(pos=(350, y), size=(80, 60), fill_color=color) + scene.children.append(frame) + frames.append((frame, easing)) + + # Markers + scene.children.append(mcrfpy.Frame(pos=(350, y - 5), size=(3, 70), fill_color=mcrfpy.Color(80, 80, 80))) + scene.children.append(mcrfpy.Frame(pos=(870, y - 5), size=(3, 70), fill_color=mcrfpy.Color(80, 80, 80))) + + frame.animate("x", 870, 3.0, easing) + +scene.children.append(mcrfpy.Caption( + text="Cubic provides stronger acceleration than quadratic", + pos=(300, 700) +)) + +def restart_animation(timer, runtime): + for frame, easing in frames: + frame.x = 350 + frame.animate("x", 870, 3.0, easing) + +loop_timer = mcrfpy.Timer("loop", restart_animation, 8000) diff --git a/tests/snippets/114_easing_sine.py b/tests/snippets/114_easing_sine.py new file mode 100644 index 0000000..2709744 --- /dev/null +++ b/tests/snippets/114_easing_sine.py @@ -0,0 +1,50 @@ +# mcrf: objects=[Caption,Color,Easing,Frame,Scene,Timer] verified=0.2.8-dev@a7ba486 status=ok +# Easing Sine - Sinusoidal easing +import mcrfpy + +scene = mcrfpy.Scene("demo") +mcrfpy.current_scene = scene + +scene.children.append(mcrfpy.Frame(pos=(0, 0), size=(1024, 768), fill_color=mcrfpy.Color(30, 30, 40))) + +_caption = mcrfpy.Caption( + text="Sine Easing - Smooth Natural Motion", + pos=(340, 50)) +_caption.fill_color = mcrfpy.Color(255, 220, 100) +scene.children.append(_caption) + +easings = [ + (mcrfpy.Easing.EASE_IN_SINE, "EASE_IN_SINE"), + (mcrfpy.Easing.EASE_OUT_SINE, "EASE_OUT_SINE"), + (mcrfpy.Easing.EASE_IN_OUT_SINE, "EASE_IN_OUT_SINE"), +] + +colors = [ + mcrfpy.Color(255, 150, 100), + mcrfpy.Color(100, 255, 150), + mcrfpy.Color(150, 100, 255), +] + +frames = [] +for i, ((easing, name), color) in enumerate(zip(easings, colors)): + y = 150 + i * 180 + + label = mcrfpy.Caption(text=name, pos=(100, y + 20)) + scene.children.append(label) + + frame = mcrfpy.Frame(pos=(350, y), size=(80, 60), fill_color=color) + scene.children.append(frame) + frames.append((frame, easing)) + frame.animate("x", 870, 3.0, easing) + +scene.children.append(mcrfpy.Caption( + text="Sine easing follows natural sinusoidal curve", + pos=(320, 700) +)) + +def restart_animation(timer, runtime): + for frame, easing in frames: + frame.x = 350 + frame.animate("x", 870, 3.0, easing) + +loop_timer = mcrfpy.Timer("loop", restart_animation, 8000) diff --git a/tests/snippets/115_easing_expo.py b/tests/snippets/115_easing_expo.py new file mode 100644 index 0000000..d6ee431 --- /dev/null +++ b/tests/snippets/115_easing_expo.py @@ -0,0 +1,50 @@ +# mcrf: objects=[Caption,Color,Easing,Frame,Scene,Timer] verified=0.2.8-dev@a7ba486 status=ok +# Easing Exponential - Dramatic acceleration +import mcrfpy + +scene = mcrfpy.Scene("demo") +mcrfpy.current_scene = scene + +scene.children.append(mcrfpy.Frame(pos=(0, 0), size=(1024, 768), fill_color=mcrfpy.Color(30, 30, 40))) + +_caption = mcrfpy.Caption( + text="Exponential Easing - Dramatic Effect", + pos=(340, 50)) +_caption.fill_color = mcrfpy.Color(255, 220, 100) +scene.children.append(_caption) + +easings = [ + (mcrfpy.Easing.EASE_IN_EXPO, "EASE_IN_EXPO"), + (mcrfpy.Easing.EASE_OUT_EXPO, "EASE_OUT_EXPO"), + (mcrfpy.Easing.EASE_IN_OUT_EXPO, "EASE_IN_OUT_EXPO"), +] + +colors = [ + mcrfpy.Color(255, 200, 100), + mcrfpy.Color(100, 200, 255), + mcrfpy.Color(200, 100, 255), +] + +frames = [] +for i, ((easing, name), color) in enumerate(zip(easings, colors)): + y = 150 + i * 180 + + label = mcrfpy.Caption(text=name, pos=(100, y + 20)) + scene.children.append(label) + + frame = mcrfpy.Frame(pos=(350, y), size=(80, 60), fill_color=color) + scene.children.append(frame) + frames.append((frame, easing)) + frame.animate("x", 870, 3.0, easing) + +scene.children.append(mcrfpy.Caption( + text="Exponential provides very sharp acceleration/deceleration", + pos=(280, 700) +)) + +def restart_animation(timer, runtime): + for frame, easing in frames: + frame.x = 350 + frame.animate("x", 870, 3.0, easing) + +loop_timer = mcrfpy.Timer("loop", restart_animation, 8000) diff --git a/tests/snippets/116_easing_circ.py b/tests/snippets/116_easing_circ.py new file mode 100644 index 0000000..a4cc7a5 --- /dev/null +++ b/tests/snippets/116_easing_circ.py @@ -0,0 +1,40 @@ +# mcrf: objects=[Caption,Color,Easing,Frame,Scene,Timer] verified=0.2.8-dev@a7ba486 status=ok +# Easing Circular - Circular motion curve +import mcrfpy + +scene = mcrfpy.Scene("demo") +mcrfpy.current_scene = scene + +scene.children.append(mcrfpy.Frame(pos=(0, 0), size=(1024, 768), fill_color=mcrfpy.Color(30, 30, 40))) + +_caption = mcrfpy.Caption( + text="Circular Easing - Arc-like Motion", + pos=(350, 50)) +_caption.fill_color = mcrfpy.Color(255, 220, 100) +scene.children.append(_caption) + +easings = [ + (mcrfpy.Easing.EASE_IN_CIRC, "EASE_IN_CIRC"), + (mcrfpy.Easing.EASE_OUT_CIRC, "EASE_OUT_CIRC"), + (mcrfpy.Easing.EASE_IN_OUT_CIRC, "EASE_IN_OUT_CIRC"), +] + +colors = [mcrfpy.Color(255, 120, 120), mcrfpy.Color(120, 255, 120), mcrfpy.Color(120, 120, 255)] + +frames = [] +for i, ((easing, name), color) in enumerate(zip(easings, colors)): + y = 150 + i * 180 + scene.children.append(mcrfpy.Caption(text=name, pos=(100, y + 20))) + frame = mcrfpy.Frame(pos=(350, y), size=(80, 60), fill_color=color) + scene.children.append(frame) + frames.append((frame, easing)) + frame.animate("x", 870, 3.0, easing) + +scene.children.append(mcrfpy.Caption(text="Circular easing follows quarter-circle curve", pos=(310, 700))) + +def restart_animation(timer, runtime): + for frame, easing in frames: + frame.x = 350 + frame.animate("x", 870, 3.0, easing) + +loop_timer = mcrfpy.Timer("loop", restart_animation, 8000) diff --git a/tests/snippets/117_easing_quart.py b/tests/snippets/117_easing_quart.py new file mode 100644 index 0000000..f2135c4 --- /dev/null +++ b/tests/snippets/117_easing_quart.py @@ -0,0 +1,40 @@ +# mcrf: objects=[Caption,Color,Easing,Frame,Scene,Timer] verified=0.2.8-dev@a7ba486 status=ok +# Easing Quartic - Strong power easing +import mcrfpy + +scene = mcrfpy.Scene("demo") +mcrfpy.current_scene = scene + +scene.children.append(mcrfpy.Frame(pos=(0, 0), size=(1024, 768), fill_color=mcrfpy.Color(30, 30, 40))) + +_caption = mcrfpy.Caption( + text="Quartic Easing (power of 4)", + pos=(380, 50)) +_caption.fill_color = mcrfpy.Color(255, 220, 100) +scene.children.append(_caption) + +easings = [ + (mcrfpy.Easing.EASE_IN_QUART, "EASE_IN_QUART"), + (mcrfpy.Easing.EASE_OUT_QUART, "EASE_OUT_QUART"), + (mcrfpy.Easing.EASE_IN_OUT_QUART, "EASE_IN_OUT_QUART"), +] + +colors = [mcrfpy.Color(200, 150, 100), mcrfpy.Color(100, 200, 150), mcrfpy.Color(150, 100, 200)] + +frames = [] +for i, ((easing, name), color) in enumerate(zip(easings, colors)): + y = 150 + i * 180 + scene.children.append(mcrfpy.Caption(text=name, pos=(100, y + 20))) + frame = mcrfpy.Frame(pos=(350, y), size=(80, 60), fill_color=color) + scene.children.append(frame) + frames.append((frame, easing)) + frame.animate("x", 870, 3.0, easing) + +scene.children.append(mcrfpy.Caption(text="Quartic: stronger than cubic, weaker than expo", pos=(290, 700))) + +def restart_animation(timer, runtime): + for frame, easing in frames: + frame.x = 350 + frame.animate("x", 870, 3.0, easing) + +loop_timer = mcrfpy.Timer("loop", restart_animation, 8000) diff --git a/tests/snippets/118_ui_modal.py b/tests/snippets/118_ui_modal.py new file mode 100644 index 0000000..2b4d0da --- /dev/null +++ b/tests/snippets/118_ui_modal.py @@ -0,0 +1,65 @@ +# mcrf: objects=[Caption,Color,Easing,Frame,InputState,Key,Scene] verified=0.2.8-dev@a7ba486 status=ok +# UI Modal Dialog - Popup confirmation +import mcrfpy + +scene = mcrfpy.Scene("demo") +mcrfpy.current_scene = scene + +scene.children.append(mcrfpy.Frame(pos=(0, 0), size=(1024, 768), fill_color=mcrfpy.Color(30, 30, 40))) + +# Background content +for i in range(5): + scene.children.append(mcrfpy.Frame( + pos=(100 + i * 180, 300), size=(140, 100), + fill_color=mcrfpy.Color(80, 80, 120) + )) + +scene.children.append(mcrfpy.Caption(text="Press SPACE to show modal", pos=(380, 600))) + +# Modal overlay (semi-transparent background) +overlay = mcrfpy.Frame( + pos=(0, 0), size=(1024, 768), + fill_color=mcrfpy.Color(0, 0, 0, 150), + visible=False +) +scene.children.append(overlay) + +# Modal dialog +modal = mcrfpy.Frame( + pos=(312, 234), size=(400, 300), + fill_color=mcrfpy.Color(50, 50, 70), + outline=3.0, + outline_color=mcrfpy.Color(100, 100, 150), + visible=False +) +scene.children.append(modal) + +_caption = mcrfpy.Caption(text="Confirm Action?", pos=(120, 40)) +_caption.fill_color = mcrfpy.Color(255, 220, 100) +modal.children.append(_caption) +modal.children.append(mcrfpy.Caption(text="Are you sure you want to proceed?", pos=(60, 120))) +modal.children.append(mcrfpy.Caption(text="Press Y=Yes, N=No", pos=(110, 200))) + +status = mcrfpy.Caption(text="", pos=(420, 650)) +scene.children.append(status) + +def show_modal(): + overlay.visible = True + modal.visible = True + modal.opacity = 0 + modal.animate("opacity", 1.0, 0.3, mcrfpy.Easing.EASE_OUT) + +def hide_modal(result): + overlay.visible = False + modal.visible = False + status.text = f"Result: {result}" + +def on_key(key, action): + if action != mcrfpy.InputState.PRESSED: return + if key == mcrfpy.Key.SPACE and not modal.visible: + show_modal() + elif modal.visible: + if key == mcrfpy.Key.Y: hide_modal("Confirmed!") + elif key == mcrfpy.Key.N: hide_modal("Cancelled") + +scene.on_key = on_key diff --git a/tests/snippets/119_game_collision.py b/tests/snippets/119_game_collision.py new file mode 100644 index 0000000..38570d1 --- /dev/null +++ b/tests/snippets/119_game_collision.py @@ -0,0 +1,68 @@ +# mcrf: objects=[Caption,Color,Entity,Grid,InputState,Key,Scene] verified=0.2.8-dev@a7ba486 status=ok +# Game Collision - Simple collision detection +import mcrfpy + +scene = mcrfpy.Scene("demo") +mcrfpy.current_scene = scene + +grid = mcrfpy.Grid( + grid_size=(16, 12), + texture=mcrfpy.default_texture, + pos=(0, 0), + size=(1024, 768), + zoom=4.0 +) +scene.children.append(grid) + +# Create walls and floor +for y in range(12): + for x in range(16): + cell = grid.at(x, y) + if x == 0 or x == 15 or y == 0 or y == 11: + cell.tilesprite = 1 + cell.walkable = False + elif x == 8 and y > 2 and y < 9: + cell.tilesprite = 1 + cell.walkable = False + else: + cell.tilesprite = 48 + cell.walkable = True + +# Player +player = mcrfpy.Entity(grid_pos=(4, 6), texture=mcrfpy.default_texture, sprite_index=84) +grid.entities.append(player) + +# Enemies +enemies = [] +for pos in [(12, 3), (12, 8)]: + e = mcrfpy.Entity(grid_pos=pos, texture=mcrfpy.default_texture, sprite_index=112) + grid.entities.append(e) + enemies.append(e) + +status = mcrfpy.Caption(text="Move with WASD - Avoid enemies!", pos=(350, 720)) +status.outline = 2 +status.outline_color = mcrfpy.Color(0, 0, 0) +scene.children.append(status) + +def check_collision(): + for e in enemies: + if e.grid_x == player.grid_x and e.grid_y == player.grid_y: + status.text = "COLLISION! Game Over!" + status.fill_color = mcrfpy.Color(255, 100, 100) + return True + return False + +def on_key(key, action): + if action != mcrfpy.InputState.PRESSED: return + x, y = player.grid_x, player.grid_y + nx, ny = x, y + if key == mcrfpy.Key.W: ny -= 1 + elif key == mcrfpy.Key.S: ny += 1 + elif key == mcrfpy.Key.A: nx -= 1 + elif key == mcrfpy.Key.D: nx += 1 + + if grid.at(nx, ny).walkable: + player.grid_pos = (nx, ny) + check_collision() + +scene.on_key = on_key diff --git a/tests/snippets/120_animation_complete.py b/tests/snippets/120_animation_complete.py new file mode 100644 index 0000000..c2a8f05 --- /dev/null +++ b/tests/snippets/120_animation_complete.py @@ -0,0 +1,45 @@ +# mcrf: objects=[Caption,Color,Easing,Frame,InputState,Key,Scene,Timer] verified=0.2.8-dev@a7ba486 status=ok +# Animation Complete Check - Monitor animation state +import mcrfpy + +scene = mcrfpy.Scene("demo") +mcrfpy.current_scene = scene + +scene.children.append(mcrfpy.Frame(pos=(0, 0), size=(1024, 768), fill_color=mcrfpy.Color(30, 30, 40))) + +frame = mcrfpy.Frame( + pos=(100, 334), size=(100, 100), + fill_color=mcrfpy.Color(255, 150, 100) +) +scene.children.append(frame) + +status = mcrfpy.Caption(text="Animation state: not started", pos=(350, 500)) +scene.children.append(status) + +elapsed_label = mcrfpy.Caption(text="", pos=(380, 550)) +scene.children.append(elapsed_label) + +scene.children.append(mcrfpy.Caption(text="Press SPACE to start animation", pos=(350, 650))) + +current_anim = None + +def update_status(timer, runtime): + if current_anim: + elapsed_label.text = f"Elapsed: {current_anim.elapsed:.2f}s / {current_anim.duration}s" + if current_anim.is_complete: + status.text = "Animation state: COMPLETE" + status.fill_color = mcrfpy.Color(100, 255, 100) + else: + status.text = "Animation state: in progress" + status.fill_color = mcrfpy.Color(255, 255, 100) + +timer = mcrfpy.Timer("check", update_status, 50) + +def on_key(key, action): + global current_anim + if key == mcrfpy.Key.SPACE and action == mcrfpy.InputState.PRESSED: + frame.x = 100 + current_anim = frame.animate("x", 800, 3.0, mcrfpy.Easing.EASE_IN_OUT) + status.text = "Animation state: started" + +scene.on_key = on_key diff --git a/tests/snippets/121_entity_at.py b/tests/snippets/121_entity_at.py new file mode 100644 index 0000000..f462354 --- /dev/null +++ b/tests/snippets/121_entity_at.py @@ -0,0 +1,48 @@ +# mcrf: objects=[Caption,Color,Entity,Grid,InputState,Scene] verified=0.2.8-dev@a7ba486 status=ok +# Entity At - Query entity's view of grid +import mcrfpy + +scene = mcrfpy.Scene("demo") +mcrfpy.current_scene = scene + +grid = mcrfpy.Grid( + grid_size=(16, 12), + texture=mcrfpy.default_texture, + pos=(0, 0), + size=(1024, 768), + zoom=4.0 +) +scene.children.append(grid) + +for y in range(12): + for x in range(16): + cell = grid.at(x, y) + if x == 8 and y > 2 and y < 9: + cell.tilesprite = 1 + cell.transparent = False + else: + cell.tilesprite = 48 + cell.transparent = True + +player = mcrfpy.Entity(grid_pos=(4, 6), texture=mcrfpy.default_texture, sprite_index=84) +grid.entities.append(player) + +grid.perspective = player +grid.fov_radius = 8 + +status = mcrfpy.Caption(text="Click to query entity's view", pos=(350, 720)) +status.outline = 2 +status.outline_color = mcrfpy.Color(0, 0, 0) +scene.children.append(status) + +def on_cell_click(pos, button, action): + if action == mcrfpy.InputState.PRESSED: + x, y = int(pos.x), int(pos.y) + # Query from entity's perspective + state = player.at(x, y) + if state: + status.text = f"({x},{y}): visible={state.visible}, discovered={state.discovered}" + else: + status.text = f"({x},{y}): out of bounds" + +grid.on_cell_click = on_cell_click diff --git a/tests/snippets/122_default_fov.py b/tests/snippets/122_default_fov.py new file mode 100644 index 0000000..7507bc7 --- /dev/null +++ b/tests/snippets/122_default_fov.py @@ -0,0 +1,53 @@ +# mcrf: objects=[Caption,Color,Entity,FOV,Frame,Grid,Scene] verified=0.2.8-dev@a7ba486 status=ok +# Default FOV - Set default algorithm +import mcrfpy + +scene = mcrfpy.Scene("demo") +mcrfpy.current_scene = scene + +scene.children.append(mcrfpy.Frame(pos=(0, 0), size=(1024, 768), fill_color=mcrfpy.Color(30, 30, 40))) + +# Display current default +current = mcrfpy.default_fov +_caption = mcrfpy.Caption( + text=f"Current default_fov: {current}", + pos=(350, 100)) +_caption.fill_color = mcrfpy.Color(255, 220, 100) +scene.children.append(_caption) + +# Change default +mcrfpy.default_fov = mcrfpy.FOV.SHADOW + +scene.children.append(mcrfpy.Caption( + text=f"Changed to: {mcrfpy.default_fov}", + pos=(380, 200) +)) + +# Create grid with default FOV +grid = mcrfpy.Grid( + grid_size=(16, 12), + texture=mcrfpy.default_texture, + pos=(212, 280), + size=(600, 400) +) +scene.children.append(grid) + +for y in range(12): + for x in range(16): + cell = grid.at(x, y) + if x == 8 and y > 2 and y < 9: + cell.tilesprite = 1 + cell.transparent = False + else: + cell.tilesprite = 48 + cell.transparent = True + +player = mcrfpy.Entity(grid_pos=(4, 6), texture=mcrfpy.default_texture, sprite_index=84) +grid.entities.append(player) +grid.perspective = player +grid.fov_radius = 8 + +status = mcrfpy.Caption(text="New grids use the default FOV algorithm", pos=(330, 700)) +status.outline = 2 +status.outline_color = mcrfpy.Color(0, 0, 0) +scene.children.append(status) diff --git a/tests/snippets/123_animation_stop.py b/tests/snippets/123_animation_stop.py new file mode 100644 index 0000000..c4df6ec --- /dev/null +++ b/tests/snippets/123_animation_stop.py @@ -0,0 +1,38 @@ +# mcrf: objects=[Caption,Color,Easing,Frame,InputState,Key,Scene] verified=0.2.8-dev@a7ba486 status=ok +# Animation Stop/Complete - Control running animations +import mcrfpy + +scene = mcrfpy.Scene("demo") +mcrfpy.current_scene = scene + +scene.children.append(mcrfpy.Frame(pos=(0, 0), size=(1024, 768), fill_color=mcrfpy.Color(30, 30, 40))) + +frame = mcrfpy.Frame( + pos=(100, 300), size=(100, 100), + fill_color=mcrfpy.Color(150, 100, 255) +) +scene.children.append(frame) + +status = mcrfpy.Caption(text="Press SPACE to start, S to stop, C to complete", pos=(280, 500)) +scene.children.append(status) + +current_anim = None + +def on_key(key, action): + global current_anim + if action != mcrfpy.InputState.PRESSED: return + + if key == mcrfpy.Key.SPACE: + frame.x = 100 + current_anim = frame.animate("x", 800, 5.0, mcrfpy.Easing.LINEAR) + status.text = "Animation started (5 seconds)" + + elif key == mcrfpy.Key.S and current_anim: + current_anim.stop() + status.text = f"Animation STOPPED at x={frame.x:.0f}" + + elif key == mcrfpy.Key.C and current_anim: + current_anim.complete() + status.text = f"Animation COMPLETED - jumped to x={frame.x:.0f}" + +scene.on_key = on_key diff --git a/tests/snippets/124_sprite_rotation.py b/tests/snippets/124_sprite_rotation.py new file mode 100644 index 0000000..a2f6071 --- /dev/null +++ b/tests/snippets/124_sprite_rotation.py @@ -0,0 +1,32 @@ +# mcrf: objects=[Caption,Color,Frame,Scene,Sprite] verified=0.2.8-dev@a7ba486 status=ok +# Sprite Rotation - Rotate sprite images +import mcrfpy + +scene = mcrfpy.Scene("demo") +mcrfpy.current_scene = scene + +scene.children.append(mcrfpy.Frame(pos=(0, 0), size=(1024, 768), fill_color=mcrfpy.Color(30, 30, 40))) + +_caption = mcrfpy.Caption( + text="Sprite Rotation", + pos=(430, 80)) +_caption.fill_color = mcrfpy.Color(255, 220, 100) +scene.children.append(_caption) + +rotations = [0, 45, 90, 135, 180, 225, 270, 315] + +for i, angle in enumerate(rotations): + x = 130 + (i % 4) * 200 + y = 200 + (i // 4) * 250 + + sprite = mcrfpy.Sprite( + texture=mcrfpy.default_texture, + sprite_index=84, + pos=(x, y), + scale=6.0 + ) + sprite.rotation = angle # Set rotation after creation + scene.children.append(sprite) + + label = mcrfpy.Caption(text=f"{angle}°", pos=(x + 30, y + 120)) + scene.children.append(label) diff --git a/tests/snippets/125_vector_normalized.py b/tests/snippets/125_vector_normalized.py new file mode 100644 index 0000000..7b1871e --- /dev/null +++ b/tests/snippets/125_vector_normalized.py @@ -0,0 +1,46 @@ +# mcrf: objects=[Caption,Color,Frame,Scene,Vector] verified=0.2.8-dev@a7ba486 status=ok +# Vector Normalized - Unit vectors +import mcrfpy + +scene = mcrfpy.Scene("demo") +mcrfpy.current_scene = scene + +scene.children.append(mcrfpy.Frame(pos=(0, 0), size=(1024, 768), fill_color=mcrfpy.Color(30, 30, 40))) + +_caption = mcrfpy.Caption( + text="Vector Normalization", + pos=(400, 80)) +_caption.fill_color = mcrfpy.Color(255, 220, 100) +scene.children.append(_caption) + +# Test vectors +vectors = [ + mcrfpy.Vector(100, 0), + mcrfpy.Vector(0, 50), + mcrfpy.Vector(30, 40), + mcrfpy.Vector(-100, 100), +] + +y = 180 +for v in vectors: + normalized = v.normalize() + + info = f"Vector({v.x:.0f}, {v.y:.0f})" + scene.children.append(mcrfpy.Caption(text=info, pos=(150, y))) + + scene.children.append(mcrfpy.Caption( + text=f"magnitude = {v.magnitude():.2f}", + pos=(400, y) + )) + + scene.children.append(mcrfpy.Caption( + text=f"normalized = ({normalized.x:.2f}, {normalized.y:.2f})", + pos=(600, y) + )) + + y += 80 + +status = mcrfpy.Caption(text="Normalized vectors have magnitude = 1.0", pos=(350, 600)) +status.outline = 2 +status.outline_color = mcrfpy.Color(0, 0, 0) +scene.children.append(status) diff --git a/tests/snippets/126_timer_restart.py b/tests/snippets/126_timer_restart.py new file mode 100644 index 0000000..2b368ed --- /dev/null +++ b/tests/snippets/126_timer_restart.py @@ -0,0 +1,48 @@ +# mcrf: objects=[Caption,Color,Frame,InputState,Key,Scene,Timer] verified=0.2.8-dev@a7ba486 status=ok +# Timer Restart - Reset and start timer +import mcrfpy + +scene = mcrfpy.Scene("demo") +mcrfpy.current_scene = scene + +scene.children.append(mcrfpy.Frame(pos=(0, 0), size=(1024, 768), fill_color=mcrfpy.Color(30, 30, 40))) + +count = 0 +count_label = mcrfpy.Caption(text="Count: 0", pos=(450, 300)) +count_label.fill_color = mcrfpy.Color(255, 220, 100) +scene.children.append(count_label) + +status = mcrfpy.Caption(text="Timer running", pos=(430, 400)) +scene.children.append(status) + +def on_tick(timer, runtime): + global count + count += 1 + count_label.text = f"Count: {count}" + +timer = mcrfpy.Timer("counter", on_tick, 1000) + +scene.children.append(mcrfpy.Caption( + text="P=pause, R=restart (resets count too)", + pos=(350, 550) +)) + +def on_key(key, action): + global count + if action != mcrfpy.InputState.PRESSED: return + + if key == mcrfpy.Key.P: + if timer.paused: + timer.resume() + status.text = "Timer resumed" + else: + timer.pause() + status.text = "Timer paused" + + elif key == mcrfpy.Key.R: + count = 0 + count_label.text = "Count: 0" + timer.restart() # Reset timer AND ensure running + status.text = "Timer restarted" + +scene.on_key = on_key diff --git a/tests/snippets/127_grid_size.py b/tests/snippets/127_grid_size.py new file mode 100644 index 0000000..0f20479 --- /dev/null +++ b/tests/snippets/127_grid_size.py @@ -0,0 +1,46 @@ +# mcrf: objects=[Caption,Color,Entity,Frame,Grid,Scene] verified=0.2.8-dev@a7ba486 status=ok +# Grid Display Size - Viewport dimensions +import mcrfpy + +scene = mcrfpy.Scene("demo") +mcrfpy.current_scene = scene + +scene.children.append(mcrfpy.Frame(pos=(0, 0), size=(1024, 768), fill_color=mcrfpy.Color(30, 30, 40))) + +# Different sized grids with same grid_size +configs = [ + ((50, 50), (300, 300), "300x300"), + ((400, 50), (500, 300), "500x300"), + ((50, 400), (300, 200), "300x200"), + ((400, 400), (500, 200), "500x200"), +] + +for pos, size, label in configs: + grid = mcrfpy.Grid( + grid_size=(10, 8), + texture=mcrfpy.default_texture, + pos=pos, + size=size + ) + scene.children.append(grid) + + for y in range(8): + for x in range(10): + grid.at(x, y).tilesprite = 48 if (x + y) % 2 == 0 else 49 + + # Add entity + grid.entities.append(mcrfpy.Entity(grid_pos=(5, 4), texture=mcrfpy.default_texture, sprite_index=84)) + + # Label + label_cap = mcrfpy.Caption(text=label, pos=(pos[0] + 10, pos[1] + size[1] + 10)) + label_cap.outline = 2 + label_cap.outline_color = mcrfpy.Color(0, 0, 0) + scene.children.append(label_cap) + +status = mcrfpy.Caption( + text="Same grid_size (10x8), different display sizes", + pos=(320, 720) +) +status.outline = 2 +status.outline_color = mcrfpy.Color(0, 0, 0) +scene.children.append(status) diff --git a/tests/snippets/128_color_animation_channels.py b/tests/snippets/128_color_animation_channels.py new file mode 100644 index 0000000..160ef63 --- /dev/null +++ b/tests/snippets/128_color_animation_channels.py @@ -0,0 +1,75 @@ +# mcrf: objects=[Caption,Color,Easing,Frame,Scene,Timer] verified=0.2.8-dev@a7ba486 status=ok +# Color Channel Animation - Animate individual RGBA +import mcrfpy + +scene = mcrfpy.Scene("demo") +mcrfpy.current_scene = scene + +scene.children.append(mcrfpy.Frame(pos=(0, 0), size=(1024, 768), fill_color=mcrfpy.Color(30, 30, 40))) + +_caption = mcrfpy.Caption( + text="Animating Color Channels", + pos=(380, 80)) +_caption.fill_color = mcrfpy.Color(255, 220, 100) +scene.children.append(_caption) + +# Red channel animation +r_frame = mcrfpy.Frame( + pos=(150, 200), size=(200, 100), + fill_color=mcrfpy.Color(128, 50, 50) +) +scene.children.append(r_frame) +r_label = mcrfpy.Caption(text="fill_color.r: 128 -> 255", pos=(150, 320)) +r_label.outline = 2 +r_label.outline_color = mcrfpy.Color(0, 0, 0) +scene.children.append(r_label) + +# Green channel animation +g_frame = mcrfpy.Frame( + pos=(400, 200), size=(200, 100), + fill_color=mcrfpy.Color(50, 128, 50) +) +scene.children.append(g_frame) +g_label = mcrfpy.Caption(text="fill_color.g: 128 -> 255", pos=(400, 320)) +g_label.outline = 2 +g_label.outline_color = mcrfpy.Color(0, 0, 0) +scene.children.append(g_label) + +# Blue channel animation +b_frame = mcrfpy.Frame( + pos=(650, 200), size=(200, 100), + fill_color=mcrfpy.Color(50, 50, 128) +) +scene.children.append(b_frame) +b_label = mcrfpy.Caption(text="fill_color.b: 128 -> 255", pos=(650, 320)) +b_label.outline = 2 +b_label.outline_color = mcrfpy.Color(0, 0, 0) +scene.children.append(b_label) + +# Alpha channel animation +a_frame = mcrfpy.Frame( + pos=(400, 450), size=(200, 100), + fill_color=mcrfpy.Color(200, 200, 200, 255) +) +scene.children.append(a_frame) +a_label = mcrfpy.Caption(text="fill_color.a: 255 -> 50", pos=(400, 570)) +a_label.outline = 2 +a_label.outline_color = mcrfpy.Color(0, 0, 0) +scene.children.append(a_label) + +def restart_animation(timer, runtime): + r_frame.fill_color = mcrfpy.Color(128, 50, 50) + g_frame.fill_color = mcrfpy.Color(50, 128, 50) + b_frame.fill_color = mcrfpy.Color(50, 50, 128) + a_frame.fill_color = mcrfpy.Color(200, 200, 200, 255) + r_frame.animate("fill_color.r", 255, 3.0, mcrfpy.Easing.LINEAR) + g_frame.animate("fill_color.g", 255, 3.0, mcrfpy.Easing.LINEAR) + b_frame.animate("fill_color.b", 255, 3.0, mcrfpy.Easing.LINEAR) + a_frame.animate("fill_color.a", 50, 3.0, mcrfpy.Easing.LINEAR) + +# Start animations +r_frame.animate("fill_color.r", 255, 3.0, mcrfpy.Easing.LINEAR) +g_frame.animate("fill_color.g", 255, 3.0, mcrfpy.Easing.LINEAR) +b_frame.animate("fill_color.b", 255, 3.0, mcrfpy.Easing.LINEAR) +a_frame.animate("fill_color.a", 50, 3.0, mcrfpy.Easing.LINEAR) +loop_timer = mcrfpy.Timer("loop", restart_animation, 8000) diff --git a/tests/snippets/129_scene_update.py b/tests/snippets/129_scene_update.py new file mode 100644 index 0000000..b064b62 --- /dev/null +++ b/tests/snippets/129_scene_update.py @@ -0,0 +1,51 @@ +# mcrf: objects=[Caption,Color,Frame,Scene] verified=0.2.8-dev@a7ba486 status=ok +# Scene Update - Per-frame update method +import mcrfpy +import math + +class GameScene(mcrfpy.Scene): + def __init__(self): + super().__init__("game") + self.time = 0 + + self.children.append(mcrfpy.Frame( + pos=(0, 0), size=(1024, 768), + fill_color=mcrfpy.Color(30, 30, 40) + )) + + _caption = mcrfpy.Caption( + text="Scene.update() - Called Every Frame", + pos=(320, 50)) + _caption.fill_color = mcrfpy.Color(255, 220, 100) + self.children.append(_caption) + + # Orbiting object + self.center = mcrfpy.Frame( + pos=(462, 334), size=(100, 100), + fill_color=mcrfpy.Color(100, 100, 150) + ) + self.children.append(self.center) + + self.orbiter = mcrfpy.Frame( + pos=(612, 334), size=(50, 50), + fill_color=mcrfpy.Color(255, 150, 100) + ) + self.children.append(self.orbiter) + + self.fps_label = mcrfpy.Caption(text="", pos=(450, 700)) + self.children.append(self.fps_label) + + def update(self, dt): + # Called every frame with delta time + self.time += dt * 2 + + # Orbit around center + radius = 150 + cx, cy = 512, 384 + self.orbiter.x = cx + math.cos(self.time) * radius - 25 + self.orbiter.y = cy + math.sin(self.time) * radius - 25 + + self.fps_label.text = f"dt={dt:.4f}s" + +scene = GameScene() +mcrfpy.current_scene = scene diff --git a/tests/snippets/130_exit_game.py b/tests/snippets/130_exit_game.py new file mode 100644 index 0000000..81c348f --- /dev/null +++ b/tests/snippets/130_exit_game.py @@ -0,0 +1,50 @@ +# mcrf: objects=[Caption,Color,Frame,InputState,Key,Scene,Timer] verified=0.2.8-dev@a7ba486 status=ok +# Exit Game - Clean shutdown +import mcrfpy + +scene = mcrfpy.Scene("demo") +mcrfpy.current_scene = scene + +scene.children.append(mcrfpy.Frame(pos=(0, 0), size=(1024, 768), fill_color=mcrfpy.Color(30, 30, 40))) + +_caption = mcrfpy.Caption( + text="Exit Game Demo", + pos=(420, 200)) +_caption.fill_color = mcrfpy.Color(255, 220, 100) +scene.children.append(_caption) + +scene.children.append(mcrfpy.Caption( + text="Press ESC to exit the game", + pos=(380, 350) +)) + +scene.children.append(mcrfpy.Caption( + text="This will close the window cleanly", + pos=(360, 420) +)) + +countdown = mcrfpy.Caption(text="", pos=(420, 550)) +scene.children.append(countdown) + +exit_countdown = 0 +exit_timer = None + +def tick(timer, runtime): + global exit_countdown + exit_countdown -= 1 + if exit_countdown > 0: + countdown.text = f"Exiting in {exit_countdown}..." + else: + timer.stop() + countdown.text = "Goodbye!" + mcrfpy.exit() # Clean shutdown + +def on_key(key, action): + global exit_countdown, exit_timer + if key == mcrfpy.Key.ESCAPE and action == mcrfpy.InputState.PRESSED: + if exit_timer is None: + countdown.text = "Exiting in 3..." + exit_countdown = 3 + exit_timer = mcrfpy.Timer("exit_timer", tick, 1000) + +scene.on_key = on_key diff --git a/tests/snippets/_harness.py b/tests/snippets/_harness.py new file mode 100644 index 0000000..27043ff --- /dev/null +++ b/tests/snippets/_harness.py @@ -0,0 +1,60 @@ +#!/usr/bin/env python3 +""" +Snippet harness -- chained as a SECOND --exec after a documentation snippet. + + ./mcrogueface --headless --exec tests/snippets/042_foo.py --exec tests/snippets/_harness.py + +The snippets under this directory are the code published on mcrogueface.github.io. +They are display scripts: each builds a scene and stops. They deliberately do NOT +call sys.exit(), because they must stay copy-pasteable -- a reader who pastes one +into their own game should get a running scene, not an interpreter that quits. + +That is exactly what makes them un-runnable on their own: since #350, a headless +--exec script that never exits is an error, because step() is the only headless +clock. So the harness supplies the ending the snippet must not have. It runs in the +same interpreter and against the same engine state, so the scene the snippet built is +still there to inspect. + +Chaining --exec (rather than exec()-ing the snippet's source from a parent script) is +deliberate on two counts: the snippet travels the same code path a real user's script +travels, and nothing has to read the file -- which is how the first version of this +harness tripped over #378 and mistook three healthy snippets for broken ones. + +A snippet PASSES if it runs clean AND leaves something behind. Merely not raising is +not enough: a snippet whose body silently no-ops still renders as a code sample on the +docs site, and would still be wrong. +""" + +import sys + +import mcrfpy + +FAIL = "SNIPPET_FAIL" +OK = "SNIPPET_OK" + + +def fail(msg): + print(f"{FAIL}: {msg}") + sys.exit(1) + + +# Advance one full simulation frame (#350): scene update, timers, Python +# Scene.update(), animations, transitions. If the snippet registered a timer or an +# animation, this is what proves it can actually tick without raising. +try: + mcrfpy.step(0.016) +except Exception as exc: # noqa: BLE001 -- any raise here is a real failure + import traceback + traceback.print_exc() + fail(f"step() raised {type(exc).__name__}: {exc}") + +scene = mcrfpy.current_scene +if scene is None: + fail("snippet left no active scene (mcrfpy.current_scene is None)") + +n = len(scene.children) +if n == 0: + fail(f"snippet activated scene {scene.name!r} but added nothing to it") + +print(f"{OK}: scene={scene.name!r} children={n}") +sys.exit(0) diff --git a/tools/stamp_snippets.py b/tools/stamp_snippets.py new file mode 100644 index 0000000..48e6d84 --- /dev/null +++ b/tools/stamp_snippets.py @@ -0,0 +1,173 @@ +#!/usr/bin/env python3 +""" +stamp_snippets.py -- run every docs snippet and stamp what actually happened. + +Plain python3, standard library only. Run from the project root: + + python3 tools/stamp_snippets.py # run, restamp headers in place + python3 tools/stamp_snippets.py --check # verify stamps are current; touch nothing + +Each snippet in tests/snippets/ carries a machine-readable provenance header: + + # mcrf: objects=[Caption,Color,Grid,Scene] verified=0.2.8@a7ba486 status=ok + +The docs site reads that header to build its snippet index -- the object tags become +cross-links, and `status` renders as a badge next to the sample. + +Until now every field was hand-typed. `status=ok` was a CLAIM, asserted by whoever last +touched the file, and 130 of them said "ok" while nothing had ever executed them. This +script makes the header a MEASUREMENT: + + * `status` comes from actually running the snippet through tests/snippets/_harness.py + * `verified` comes from the engine that ran it (version@commit, out of api/manifest.json) + * `objects` is derived from the source, intersected with the objects the engine + actually exports -- so a snippet that starts using a new type gets tagged + for it without anyone remembering to. + +--check is the CI gate: it fails if any stamp disagrees with a fresh run, which is what +stops the corpus from quietly drifting back into decoration. +""" + +import argparse +import json +import os +import re +import subprocess +import sys + +ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) +SNIPPET_DIR = os.path.join(ROOT, "tests", "snippets") +HARNESS = os.path.join(SNIPPET_DIR, "_harness.py") +MANIFEST = os.path.join(ROOT, "api", "manifest.json") +BUILD_DIR = os.path.join(ROOT, "build") +MCROGUEFACE = os.path.join(BUILD_DIR, "mcrogueface") + +HEADER_RE = re.compile(r"^#\s*mcrf:\s*(.*)$") +TIMEOUT = 30 + + +def load_manifest(): + with open(MANIFEST, encoding="utf-8") as f: + m = json.load(f) + version = m.get("version", "unknown") + commit = m.get("commit", "unknown") + objects = set(m.get("objects", {}).keys()) + if not objects: + sys.exit("manifest has no objects -- regenerate api/manifest.json first") + return version, commit, objects + + +def snippet_paths(): + names = sorted( + n for n in os.listdir(SNIPPET_DIR) + if n.endswith(".py") and not n.startswith("_") + ) + return [os.path.join(SNIPPET_DIR, n) for n in names] + + +def run_snippet(path): + """Run one snippet through the harness. Returns (status, detail).""" + env = os.environ.copy() + lib = os.path.join(BUILD_DIR, "lib") + env["LD_LIBRARY_PATH"] = os.pathsep.join( + p for p in (lib, env.get("LD_LIBRARY_PATH", "")) if p + ) + try: + proc = subprocess.run( + [MCROGUEFACE, "--headless", "--exec", path, "--exec", HARNESS], + capture_output=True, text=True, timeout=TIMEOUT, cwd=BUILD_DIR, env=env, + ) + except subprocess.TimeoutExpired: + return "fail", "timeout" + + out = proc.stdout + proc.stderr + if "Traceback (most recent call last)" in out: + last = [l for l in out.strip().splitlines() if l.strip()] + return "fail", last[-1][:120] if last else "traceback" + if "SNIPPET_FAIL" in out: + line = next((l for l in out.splitlines() if "SNIPPET_FAIL" in l), "") + return "fail", line.split("SNIPPET_FAIL:", 1)[-1].strip()[:120] + if proc.returncode != 0: + return "fail", f"exit {proc.returncode}" + if "SNIPPET_OK" not in out: + return "fail", "harness did not report SNIPPET_OK" + return "ok", "" + + +def derive_objects(source, known): + """Which engine objects does this snippet actually touch?""" + used = set(re.findall(r"\bmcrfpy\.([A-Z]\w*)", source)) + return sorted(used & known) + + +def build_header(objects, version, commit, status): + return (f"# mcrf: objects=[{','.join(objects)}] " + f"verified={version}@{commit} status={status}") + + +def restamp(source, header): + """Replace the leading `# mcrf:` line, or insert one at the top.""" + lines = source.splitlines(keepends=True) + for i, line in enumerate(lines[:5]): + if HEADER_RE.match(line.rstrip("\n")): + lines[i] = header + "\n" + return "".join(lines) + return header + "\n" + "".join(lines) + + +def main(): + ap = argparse.ArgumentParser() + ap.add_argument("--check", action="store_true", + help="fail if any stamp is out of date; write nothing") + args = ap.parse_args() + + if not os.path.exists(MCROGUEFACE): + sys.exit(f"{MCROGUEFACE} not found -- run `make` first") + + version, commit, known = load_manifest() + paths = snippet_paths() + print(f"stamping {len(paths)} snippets against {version}@{commit}\n") + + failed, stale = [], [] + for path in paths: + name = os.path.basename(path) + with open(path, encoding="utf-8") as f: + source = f.read() + + status, detail = run_snippet(path) + header = build_header(derive_objects(source, known), version, commit, status) + updated = restamp(source, header) + + if status == "fail": + failed.append((name, detail)) + print(f" FAIL {name}: {detail}") + + if updated != source: + if args.check: + stale.append(name) + else: + with open(path, "w", encoding="utf-8") as f: + f.write(updated) + + print(f"\n{len(paths) - len(failed)}/{len(paths)} snippets ok") + + if args.check and stale: + print(f"\n{len(stale)} snippet(s) have a stale `# mcrf:` stamp:") + for name in stale: + print(f" - {name}") + print("\nRun: python3 tools/stamp_snippets.py") + return 1 + + if failed: + print(f"\n{len(failed)} snippet(s) failed:") + for name, detail in failed: + print(f" - {name}: {detail}") + return 1 + + if not args.check: + print("headers restamped.") + return 0 + + +if __name__ == "__main__": + sys.exit(main()) From 50d6f1e5d2e7daad824880ef906e301022cefcda Mon Sep 17 00:00:00 2001 From: John McCardle Date: Tue, 14 Jul 2026 18:33:31 -0400 Subject: [PATCH 3/5] build(docs): chain the release documentation pipeline into `make release-docs` Every piece of this pipeline already existed -- generate_all_docs.sh, generate_api_manifest.py, api_delta.py (with its --site-dir page resolver and its --format gitea checklist), and the site's build_reference.py / build_library.py, which already knew how to read this repo. Nothing chained them. So the site's generated reference sat 26 engine commits behind, stamped verified_commit b6720f6, and nothing noticed or could have noticed. make docs regenerate man page, API reference, stubs, manifest make stamp-snippets re-run the docs snippets; stamp what actually happened make check-snippets CI gate: fail on a broken snippet or a stale stamp make api-delta what changed, and which site pages that obligates make release-docs all of it, for cutting a tag release-docs deliberately does not commit and does not touch the hand-written pages. It refreshes what is derived, proves the published samples still run, rebuilds the site's generated half against this engine, and prints the checklist of curated pages the API change obliges you to revisit. The judgment stays with a human; only the mechanical parts are mechanical. Two ref-shaped traps, both hit while building this: * BASE_REF and RELEASE_REF are different refs and I had conflated them. The delta is measured FROM the previous release; the site's source links are pinned TO the tag being cut. One variable could not be both. * api_delta now distinguishes "this ref predates the manifest" from "this ref is broken". 0.2.8 is older than the manifest infra (54624b3), so it has no baseline -- every object would read as "added", which is noise, not a delta. It says so and exits clean. An unknown ref is still an error. stamp_snippets --check compares only the CLAIMS a header makes (status, objects), never `verified`. That field records the engine a snippet was last confirmed against -- provenance, not a freshness assertion. Had --check demanded it equal HEAD, every commit would have invalidated all 130 stamps and the gate would have cried wolf until people stopped listening. Running it regenerated all 52 reference pages against the current engine, which cleared every stale UIGrid / compute_astar / gridstate mention the docs audit found in the generated tree -- the engine's docstrings had been right for weeks; the site had simply never been rebuilt. --- Makefile | 72 +++++++++++++++++++++++++++++++++++++++++ tools/api_delta.py | 17 ++++++++++ tools/stamp_snippets.py | 50 ++++++++++++++++++++++------ 3 files changed, 130 insertions(+), 9 deletions(-) diff --git a/Makefile b/Makefile index 32f12c6..18a2c02 100644 --- a/Makefile +++ b/Makefile @@ -96,6 +96,78 @@ install-hooks: echo "Linked $$hooks_dir/$$name -> $$hook"; \ done +# --------------------------------------------------------------------------- +# Documentation / release +# +# Every piece of the docs pipeline already existed; nothing chained them, so the +# site drifted 26 engine commits behind without anything noticing. These targets +# are the chain. +# +# SITE_DIR is the mcrogueface.github.io checkout. Override if yours lives elsewhere: +# make release-docs SITE_DIR=/path/to/mcrogueface.github.io +# --------------------------------------------------------------------------- +SITE_DIR ?= $(CURDIR)/../mcrogueface.github.io + +# Two DIFFERENT refs, easily conflated: +# +# BASE_REF the PREVIOUS release. The API delta is measured against it, to answer +# "what changed, and which pages does that oblige us to revisit?" +# RELEASE_REF the tag being CUT. The site pins its source links to it, which is what +# freezes this version into the site's history. +# +# When cutting a release, set both: +# make release-docs BASE_REF=0.2.8 RELEASE_REF=0.2.9 +# +# Note: BASE_REF must carry api/manifest.json. The manifest infrastructure landed in +# 54624b3, so tags older than that (0.2.8 included) have no baseline to diff against -- +# api_delta will say so rather than inventing one. +BASE_REF ?= $(shell git describe --tags --abbrev=0 2>/dev/null) +RELEASE_REF ?= $(shell git describe --tags --always --abbrev=0 2>/dev/null) + +# Regenerate everything derived from the compiled module: man page, API reference +# (HTML + Markdown), type stubs, and the tracked api/manifest.json. +docs: linux + @./tools/generate_all_docs.sh + @./build/mcrogueface --headless --exec ../tools/generate_api_manifest.py + +# Re-run every docs snippet and stamp its `# mcrf:` header with what actually +# happened (status from the run, verified from the engine, objects from the source). +stamp-snippets: linux + @python3 tools/stamp_snippets.py + +# CI gate: fail if any snippet is broken or its stamp is stale. Writes nothing. +check-snippets: linux + @python3 tools/stamp_snippets.py --check + +# What changed in the API since the last release, and which site pages that +# obligates you to update (resolved via each page's `mcrf.objects` frontmatter). +api-delta: + @python3 tools/api_delta.py $(BASE_REF) . --site-dir $(SITE_DIR) --format md + +# The full release documentation pass. Run this when cutting a tag: it refreshes +# everything the engine derives, proves the published samples still run, rebuilds +# the site's generated reference + snippet library against this engine, and prints +# the checklist of hand-written pages the API change obligates you to revisit. +# +# Deliberately NOT automatic: it does not commit, and it does not touch the +# hand-written pages. It tells you what it found and leaves the judgment to you. +release-docs: docs stamp-snippets + @echo "" + @echo "=== Regenerating the site against this engine (SITE_DIR=$(SITE_DIR)) ===" + @test -d "$(SITE_DIR)" || { echo "SITE_DIR not found: $(SITE_DIR)"; exit 1; } + @cd "$(SITE_DIR)" && python3 tools/build_reference.py + @cd "$(SITE_DIR)" && python3 tools/build_library.py --ref $(RELEASE_REF) + @echo "" + @echo "=== API delta $(BASE_REF) -> now, and the pages it obligates ===" + @python3 tools/api_delta.py $(BASE_REF) . --site-dir $(SITE_DIR) --format md + @echo "" + @echo "Site pinned to RELEASE_REF=$(RELEASE_REF); delta measured from BASE_REF=$(BASE_REF)." + @echo "Next: review the delta above, update the hand-written pages it names," + @echo " then commit both repos. For a Gitea checklist issue instead, run:" + @echo " python3 tools/api_delta.py $(BASE_REF) . --site-dir $(SITE_DIR) --format gitea" + +.PHONY: docs stamp-snippets check-snippets api-delta release-docs + # Debug and sanitizer targets debug: @echo "Building McRogueFace with debug Python (pydebug assertions)..." diff --git a/tools/api_delta.py b/tools/api_delta.py index b9db5b6..21d8ce9 100755 --- a/tools/api_delta.py +++ b/tools/api_delta.py @@ -58,6 +58,14 @@ def load_manifest(ref, repo): return json.load(f) rc, out, err = git_output(["show", "%s:api/manifest.json" % ref], repo) if rc != 0: + # Distinguish "this ref predates the manifest" from "the ref is broken". The + # manifest infrastructure landed in 54624b3, so every tag older than that -- + # 0.2.8 included -- genuinely has no API baseline. That is a fact to report, + # not a crash: there is nothing to diff against, and saying so is the honest + # answer to "what changed since 0.2.8?". An unknown ref is still an error. + rc_ref, _, _ = git_output(["rev-parse", "--verify", "%s^{commit}" % ref], repo) + if rc_ref == 0: + return None raise SystemExit("error: cannot read api/manifest.json at ref '%s': %s" % (ref, err.strip())) return json.loads(out) @@ -409,6 +417,15 @@ def main(argv=None): a = load_manifest(args.ref1, repo) b = load_manifest(args.ref2, repo) + if a is None: + print("# API delta: %s -> %s\n" % (args.ref1, args.ref2)) + print("**No baseline.** `%s` predates the API manifest (added in 54624b3), so " + "there is nothing to diff against -- every object in the current API would " + "read as \"added\", which is noise rather than a delta.\n" + % args.ref1) + print("This release establishes the baseline: the next one can diff against it.") + return 0 + object_pages = scan_site(args.site_dir) if args.site_dir else None delta = compute_delta(a, b, object_pages) diff --git a/tools/stamp_snippets.py b/tools/stamp_snippets.py index 48e6d84..a95e15a 100644 --- a/tools/stamp_snippets.py +++ b/tools/stamp_snippets.py @@ -94,6 +94,25 @@ def run_snippet(path): return "ok", "" +def parse_header(source): + """Read the existing `# mcrf:` claims back out of a snippet.""" + for line in source.splitlines()[:5]: + m = HEADER_RE.match(line.rstrip("\n")) + if not m: + continue + payload = m.group(1) + out = {} + objs = re.search(r"objects=\[([^\]]*)\]", payload) + out["objects"] = ([o.strip() for o in objs.group(1).split(",") if o.strip()] + if objs else []) + st = re.search(r"status=(\S+)", payload) + out["status"] = st.group(1) if st else None + ver = re.search(r"verified=(\S+)", payload) + out["verified"] = ver.group(1) if ver else None + return out + return {"objects": [], "status": None, "verified": None} + + def derive_objects(source, known): """Which engine objects does this snippet actually touch?""" used = set(re.findall(r"\bmcrfpy\.([A-Z]\w*)", source)) @@ -135,26 +154,39 @@ def main(): source = f.read() status, detail = run_snippet(path) - header = build_header(derive_objects(source, known), version, commit, status) - updated = restamp(source, header) + objects = derive_objects(source, known) if status == "fail": failed.append((name, detail)) print(f" FAIL {name}: {detail}") + if args.check: + # Compare only the CLAIMS -- status and objects. Deliberately NOT `verified`: + # that field records the engine a snippet was last confirmed against, which + # is provenance, not a freshness assertion. If --check demanded it equal + # HEAD, every commit would invalidate all 130 stamps and the gate would cry + # wolf until people stopped listening. `verified` is refreshed at release + # time by `make stamp-snippets`. + old = parse_header(source) + if old.get("status") != status: + stale.append(f"{name}: header says status={old.get('status')!r}, " + f"actual run is {status!r}") + elif old.get("objects") != objects: + stale.append(f"{name}: header objects {old.get('objects')} " + f"!= actual {objects}") + continue + + updated = restamp(source, build_header(objects, version, commit, status)) if updated != source: - if args.check: - stale.append(name) - else: - with open(path, "w", encoding="utf-8") as f: - f.write(updated) + with open(path, "w", encoding="utf-8") as f: + f.write(updated) print(f"\n{len(paths) - len(failed)}/{len(paths)} snippets ok") if args.check and stale: print(f"\n{len(stale)} snippet(s) have a stale `# mcrf:` stamp:") - for name in stale: - print(f" - {name}") + for line in stale: + print(f" - {line}") print("\nRun: python3 tools/stamp_snippets.py") return 1 From 3a85dd818d1d001b0bec97707a872a7e5ac8eb3b Mon Sep 17 00:00:00 2001 From: John McCardle Date: Tue, 14 Jul 2026 19:14:49 -0400 Subject: [PATCH 4/5] fix(snippets): drop the commit hash from `verified` -- it churned all 130 every commit The `# mcrf:` stamp recorded verified=@, so every commit rewrote the hash in all 130 snippet headers -- a guaranteed 130-file diff carrying no signal git history doesn't already have, and one `--check` ignores anyway. The version alone is stable within a dev cycle ("0.2.8-dev") and becomes the release marker at a tag ("0.2.9"), which is the whole point of the field: freezing a version into the docs site's history, not pinning a commit. verified is now version-only. The stamper is idempotent again: a second run on a clean tree changes zero files. This commit is the one-time normalization that strips the @hash suffix from the existing headers. Found while cleaning up after a make release-docs run left all 130 snippets dirty with nothing but hash churn. --- tests/snippets/001_hello_frame.py | 2 +- tests/snippets/002_hello_caption.py | 2 +- tests/snippets/003_hello_sprite.py | 2 +- tests/snippets/004_hello_grid.py | 2 +- tests/snippets/005_hello_entity.py | 2 +- tests/snippets/006_frame_colors.py | 2 +- tests/snippets/007_frame_opacity.py | 2 +- tests/snippets/008_frame_outline.py | 2 +- tests/snippets/009_frame_children.py | 2 +- tests/snippets/010_frame_clip_children.py | 2 +- tests/snippets/011_frame_z_index.py | 2 +- tests/snippets/012_frame_visible.py | 2 +- tests/snippets/013_frame_name.py | 2 +- tests/snippets/014_frame_rotation.py | 2 +- tests/snippets/015_frame_origin.py | 2 +- tests/snippets/016_frame_alignment.py | 2 +- tests/snippets/017_caption_styling.py | 2 +- tests/snippets/018_caption_in_frame.py | 2 +- tests/snippets/019_caption_dimensions.py | 2 +- tests/snippets/020_sprite_atlas.py | 2 +- tests/snippets/021_sprite_scale.py | 2 +- tests/snippets/022_sprite_tint.py | 2 +- tests/snippets/023_grid_tiles.py | 2 +- tests/snippets/024_grid_walkable.py | 2 +- tests/snippets/025_grid_cell_color.py | 2 +- tests/snippets/026_grid_camera_center.py | 2 +- tests/snippets/027_grid_zoom.py | 2 +- tests/snippets/028_entity_movement.py | 2 +- tests/snippets/029_entity_multiple.py | 2 +- tests/snippets/030_entity_draw_pos.py | 2 +- tests/snippets/031_animation_basic.py | 2 +- tests/snippets/032_animation_easing_linear.py | 2 +- tests/snippets/033_animation_easing_ease_in.py | 2 +- tests/snippets/034_animation_easing_ease_out.py | 2 +- tests/snippets/035_animation_easing_ease_in_out.py | 2 +- tests/snippets/036_animation_easing_bounce.py | 2 +- tests/snippets/037_animation_easing_elastic.py | 2 +- tests/snippets/038_animation_easing_back.py | 2 +- tests/snippets/039_animation_easing_comparison.py | 2 +- tests/snippets/040_animation_color.py | 2 +- tests/snippets/041_animation_opacity.py | 2 +- tests/snippets/042_animation_size.py | 2 +- tests/snippets/043_animation_sprite_index.py | 2 +- tests/snippets/044_animation_callback.py | 2 +- tests/snippets/045_animation_chain.py | 2 +- tests/snippets/046_timer_basic.py | 2 +- tests/snippets/047_timer_once.py | 2 +- tests/snippets/048_timer_control.py | 2 +- tests/snippets/049_input_keyboard.py | 2 +- tests/snippets/050_input_mouse_click.py | 2 +- tests/snippets/051_input_mouse_hover.py | 2 +- tests/snippets/052_input_grid_cell_click.py | 2 +- tests/snippets/053_input_grid_cell_hover.py | 2 +- tests/snippets/054_pathfinding_astar.py | 2 +- tests/snippets/055_pathfinding_entity.py | 2 +- tests/snippets/056_pathfinding_dijkstra.py | 2 +- tests/snippets/057_fov_basic.py | 2 +- tests/snippets/058_fov_algorithms.py | 2 +- tests/snippets/059_fov_perspective.py | 2 +- tests/snippets/060_bsp_basic.py | 2 +- tests/snippets/061_scene_switching.py | 2 +- tests/snippets/062_scene_transition_fade.py | 2 +- tests/snippets/063_scene_transition_slide.py | 2 +- tests/snippets/064_vector_basics.py | 2 +- tests/snippets/065_color_components.py | 2 +- tests/snippets/066_find_elements.py | 2 +- tests/snippets/067_collection_iteration.py | 2 +- tests/snippets/068_entity_collection.py | 2 +- tests/snippets/069_window_properties.py | 2 +- tests/snippets/070_keyboard_state.py | 2 +- tests/snippets/071_mouse_state.py | 2 +- tests/snippets/072_ui_button.py | 2 +- tests/snippets/073_ui_health_bar.py | 2 +- tests/snippets/074_ui_tooltip.py | 2 +- tests/snippets/075_ui_message_log.py | 2 +- tests/snippets/076_effect_screen_shake.py | 2 +- tests/snippets/077_effect_damage_flash.py | 2 +- tests/snippets/078_effect_floating_text.py | 2 +- tests/snippets/079_effect_pulse.py | 2 +- tests/snippets/080_game_turn_based.py | 2 +- tests/snippets/081_game_inventory.py | 2 +- tests/snippets/082_game_minimap.py | 2 +- tests/snippets/083_grid_layers.py | 2 +- tests/snippets/084_bresenham_line.py | 2 +- tests/snippets/085_entity_visibility.py | 2 +- tests/snippets/086_procgen_cellular.py | 2 +- tests/snippets/087_grid_point_state.py | 2 +- tests/snippets/088_animation_multiple.py | 2 +- tests/snippets/089_camera_follow.py | 2 +- tests/snippets/090_scene_lifecycle.py | 2 +- tests/snippets/091_metrics_display.py | 2 +- tests/snippets/092_global_pos.py | 2 +- tests/snippets/093_parent_reference.py | 2 +- tests/snippets/094_entity_die.py | 2 +- tests/snippets/095_entity_index.py | 2 +- tests/snippets/096_timer_remaining.py | 2 +- tests/snippets/097_hovered_state.py | 2 +- tests/snippets/098_grid_fill_color.py | 2 +- tests/snippets/099_scene_pos.py | 2 +- tests/snippets/100_complete_game.py | 2 +- tests/snippets/101_grid_camera_rotation.py | 2 +- tests/snippets/102_bsp_traversal.py | 2 +- tests/snippets/103_animation_pos_vector.py | 2 +- tests/snippets/104_texture_properties.py | 2 +- tests/snippets/105_scenes_dict.py | 2 +- tests/snippets/106_timers_list.py | 2 +- tests/snippets/107_animations_list.py | 2 +- tests/snippets/108_cache_subtree.py | 2 +- tests/snippets/109_realign.py | 2 +- tests/snippets/110_version_info.py | 2 +- tests/snippets/111_alignment_margins.py | 2 +- tests/snippets/112_animation_delta.py | 2 +- tests/snippets/113_easing_cubic.py | 2 +- tests/snippets/114_easing_sine.py | 2 +- tests/snippets/115_easing_expo.py | 2 +- tests/snippets/116_easing_circ.py | 2 +- tests/snippets/117_easing_quart.py | 2 +- tests/snippets/118_ui_modal.py | 2 +- tests/snippets/119_game_collision.py | 2 +- tests/snippets/120_animation_complete.py | 2 +- tests/snippets/121_entity_at.py | 2 +- tests/snippets/122_default_fov.py | 2 +- tests/snippets/123_animation_stop.py | 2 +- tests/snippets/124_sprite_rotation.py | 2 +- tests/snippets/125_vector_normalized.py | 2 +- tests/snippets/126_timer_restart.py | 2 +- tests/snippets/127_grid_size.py | 2 +- tests/snippets/128_color_animation_channels.py | 2 +- tests/snippets/129_scene_update.py | 2 +- tests/snippets/130_exit_game.py | 2 +- tools/stamp_snippets.py | 14 ++++++++++---- 131 files changed, 140 insertions(+), 134 deletions(-) diff --git a/tests/snippets/001_hello_frame.py b/tests/snippets/001_hello_frame.py index 3fcf09e..876df89 100644 --- a/tests/snippets/001_hello_frame.py +++ b/tests/snippets/001_hello_frame.py @@ -1,4 +1,4 @@ -# mcrf: objects=[Color,Frame,Scene] verified=0.2.8-dev@a7ba486 status=ok +# mcrf: objects=[Color,Frame,Scene] verified=0.2.8-dev status=ok # Hello Frame - The simplest UI element import mcrfpy diff --git a/tests/snippets/002_hello_caption.py b/tests/snippets/002_hello_caption.py index 94f2997..9c3d0c7 100644 --- a/tests/snippets/002_hello_caption.py +++ b/tests/snippets/002_hello_caption.py @@ -1,4 +1,4 @@ -# mcrf: objects=[Caption,Color,Scene] verified=0.2.8-dev@a7ba486 status=ok +# mcrf: objects=[Caption,Color,Scene] verified=0.2.8-dev status=ok # Hello Caption - Display text on screen import mcrfpy diff --git a/tests/snippets/003_hello_sprite.py b/tests/snippets/003_hello_sprite.py index cf057a1..bac8623 100644 --- a/tests/snippets/003_hello_sprite.py +++ b/tests/snippets/003_hello_sprite.py @@ -1,4 +1,4 @@ -# mcrf: objects=[Caption,Scene,Sprite] verified=0.2.8-dev@a7ba486 status=ok +# mcrf: objects=[Caption,Scene,Sprite] verified=0.2.8-dev status=ok # Hello Sprite - Display a textured sprite import mcrfpy diff --git a/tests/snippets/004_hello_grid.py b/tests/snippets/004_hello_grid.py index 5c413ad..527a57c 100644 --- a/tests/snippets/004_hello_grid.py +++ b/tests/snippets/004_hello_grid.py @@ -1,4 +1,4 @@ -# mcrf: objects=[Grid,Scene] verified=0.2.8-dev@a7ba486 status=ok +# mcrf: objects=[Grid,Scene] verified=0.2.8-dev status=ok # Hello Grid - Create a tile-based grid import mcrfpy diff --git a/tests/snippets/005_hello_entity.py b/tests/snippets/005_hello_entity.py index 7bedb23..cad2f01 100644 --- a/tests/snippets/005_hello_entity.py +++ b/tests/snippets/005_hello_entity.py @@ -1,4 +1,4 @@ -# mcrf: objects=[Entity,Grid,Scene] verified=0.2.8-dev@a7ba486 status=ok +# mcrf: objects=[Entity,Grid,Scene] verified=0.2.8-dev status=ok # Hello Entity - Add entities to a grid import mcrfpy diff --git a/tests/snippets/006_frame_colors.py b/tests/snippets/006_frame_colors.py index 8d07637..a8592e2 100644 --- a/tests/snippets/006_frame_colors.py +++ b/tests/snippets/006_frame_colors.py @@ -1,4 +1,4 @@ -# mcrf: objects=[Caption,Color,Frame,Scene] verified=0.2.8-dev@a7ba486 status=ok +# mcrf: objects=[Caption,Color,Frame,Scene] verified=0.2.8-dev status=ok # Frame Colors - Fill and outline colors import mcrfpy diff --git a/tests/snippets/007_frame_opacity.py b/tests/snippets/007_frame_opacity.py index 97399c8..ae06a5f 100644 --- a/tests/snippets/007_frame_opacity.py +++ b/tests/snippets/007_frame_opacity.py @@ -1,4 +1,4 @@ -# mcrf: objects=[Caption,Color,Frame,Scene] verified=0.2.8-dev@a7ba486 status=ok +# mcrf: objects=[Caption,Color,Frame,Scene] verified=0.2.8-dev status=ok # Frame Opacity - Transparency levels import mcrfpy diff --git a/tests/snippets/008_frame_outline.py b/tests/snippets/008_frame_outline.py index cf79631..c7e2ea7 100644 --- a/tests/snippets/008_frame_outline.py +++ b/tests/snippets/008_frame_outline.py @@ -1,4 +1,4 @@ -# mcrf: objects=[Caption,Color,Frame,Scene] verified=0.2.8-dev@a7ba486 status=ok +# mcrf: objects=[Caption,Color,Frame,Scene] verified=0.2.8-dev status=ok # Frame Outline - Border thickness import mcrfpy diff --git a/tests/snippets/009_frame_children.py b/tests/snippets/009_frame_children.py index deff54f..ec4ea1b 100644 --- a/tests/snippets/009_frame_children.py +++ b/tests/snippets/009_frame_children.py @@ -1,4 +1,4 @@ -# mcrf: objects=[Caption,Color,Frame,Scene] verified=0.2.8-dev@a7ba486 status=ok +# mcrf: objects=[Caption,Color,Frame,Scene] verified=0.2.8-dev status=ok # Frame Children - Nested UI elements import mcrfpy diff --git a/tests/snippets/010_frame_clip_children.py b/tests/snippets/010_frame_clip_children.py index 9cc812e..bb4e396 100644 --- a/tests/snippets/010_frame_clip_children.py +++ b/tests/snippets/010_frame_clip_children.py @@ -1,4 +1,4 @@ -# mcrf: objects=[Caption,Color,Frame,Scene] verified=0.2.8-dev@a7ba486 status=ok +# mcrf: objects=[Caption,Color,Frame,Scene] verified=0.2.8-dev status=ok # Frame Clipping - clip_children property import mcrfpy diff --git a/tests/snippets/011_frame_z_index.py b/tests/snippets/011_frame_z_index.py index 14f4a83..8263fa5 100644 --- a/tests/snippets/011_frame_z_index.py +++ b/tests/snippets/011_frame_z_index.py @@ -1,4 +1,4 @@ -# mcrf: objects=[Caption,Color,Frame,Scene] verified=0.2.8-dev@a7ba486 status=ok +# mcrf: objects=[Caption,Color,Frame,Scene] verified=0.2.8-dev status=ok # Frame Z-Index - Control rendering order import mcrfpy diff --git a/tests/snippets/012_frame_visible.py b/tests/snippets/012_frame_visible.py index 77c1a7d..4b4f91c 100644 --- a/tests/snippets/012_frame_visible.py +++ b/tests/snippets/012_frame_visible.py @@ -1,4 +1,4 @@ -# mcrf: objects=[Caption,Color,Frame,Scene] verified=0.2.8-dev@a7ba486 status=ok +# mcrf: objects=[Caption,Color,Frame,Scene] verified=0.2.8-dev status=ok # Frame Visibility - Show and hide elements import mcrfpy diff --git a/tests/snippets/013_frame_name.py b/tests/snippets/013_frame_name.py index 5d313f5..44e2b4e 100644 --- a/tests/snippets/013_frame_name.py +++ b/tests/snippets/013_frame_name.py @@ -1,4 +1,4 @@ -# mcrf: objects=[Caption,Color,Frame,Scene] verified=0.2.8-dev@a7ba486 status=ok +# mcrf: objects=[Caption,Color,Frame,Scene] verified=0.2.8-dev status=ok # Frame Name - Named elements for lookup import mcrfpy diff --git a/tests/snippets/014_frame_rotation.py b/tests/snippets/014_frame_rotation.py index 3ebcb51..c1d00a9 100644 --- a/tests/snippets/014_frame_rotation.py +++ b/tests/snippets/014_frame_rotation.py @@ -1,4 +1,4 @@ -# mcrf: objects=[Caption,Color,Frame,Scene] verified=0.2.8-dev@a7ba486 status=ok +# mcrf: objects=[Caption,Color,Frame,Scene] verified=0.2.8-dev status=ok # Frame Rotation - Rotate UI elements import mcrfpy diff --git a/tests/snippets/015_frame_origin.py b/tests/snippets/015_frame_origin.py index 0ada55d..8af606c 100644 --- a/tests/snippets/015_frame_origin.py +++ b/tests/snippets/015_frame_origin.py @@ -1,4 +1,4 @@ -# mcrf: objects=[Caption,Color,Frame,Scene,Vector] verified=0.2.8-dev@a7ba486 status=ok +# mcrf: objects=[Caption,Color,Frame,Scene,Vector] verified=0.2.8-dev status=ok # Frame Origin - Rotation pivot point import mcrfpy diff --git a/tests/snippets/016_frame_alignment.py b/tests/snippets/016_frame_alignment.py index 949edba..af12b07 100644 --- a/tests/snippets/016_frame_alignment.py +++ b/tests/snippets/016_frame_alignment.py @@ -1,4 +1,4 @@ -# mcrf: objects=[Alignment,Color,Frame,Scene] verified=0.2.8-dev@a7ba486 status=ok +# mcrf: objects=[Alignment,Color,Frame,Scene] verified=0.2.8-dev status=ok # Frame Alignment - Automatic positioning import mcrfpy diff --git a/tests/snippets/017_caption_styling.py b/tests/snippets/017_caption_styling.py index 2df4d62..5f0a934 100644 --- a/tests/snippets/017_caption_styling.py +++ b/tests/snippets/017_caption_styling.py @@ -1,4 +1,4 @@ -# mcrf: objects=[Caption,Color,Scene] verified=0.2.8-dev@a7ba486 status=ok +# mcrf: objects=[Caption,Color,Scene] verified=0.2.8-dev status=ok # Caption Styling - Text colors and fonts import mcrfpy diff --git a/tests/snippets/018_caption_in_frame.py b/tests/snippets/018_caption_in_frame.py index 22bef59..7fdb48a 100644 --- a/tests/snippets/018_caption_in_frame.py +++ b/tests/snippets/018_caption_in_frame.py @@ -1,4 +1,4 @@ -# mcrf: objects=[Caption,Color,Frame,Scene] verified=0.2.8-dev@a7ba486 status=ok +# mcrf: objects=[Caption,Color,Frame,Scene] verified=0.2.8-dev status=ok # Caption in Frame - Text inside containers import mcrfpy diff --git a/tests/snippets/019_caption_dimensions.py b/tests/snippets/019_caption_dimensions.py index d2e72e7..27be611 100644 --- a/tests/snippets/019_caption_dimensions.py +++ b/tests/snippets/019_caption_dimensions.py @@ -1,4 +1,4 @@ -# mcrf: objects=[Caption,Color,Frame,Scene] verified=0.2.8-dev@a7ba486 status=ok +# mcrf: objects=[Caption,Color,Frame,Scene] verified=0.2.8-dev status=ok # Caption Dimensions - Text width and height import mcrfpy diff --git a/tests/snippets/020_sprite_atlas.py b/tests/snippets/020_sprite_atlas.py index c24fef4..f0a150f 100644 --- a/tests/snippets/020_sprite_atlas.py +++ b/tests/snippets/020_sprite_atlas.py @@ -1,4 +1,4 @@ -# mcrf: objects=[Caption,Color,Scene,Sprite] verified=0.2.8-dev@a7ba486 status=ok +# mcrf: objects=[Caption,Color,Scene,Sprite] verified=0.2.8-dev status=ok # Sprite Atlas - Browse texture atlas import mcrfpy diff --git a/tests/snippets/021_sprite_scale.py b/tests/snippets/021_sprite_scale.py index cf58af6..de86c02 100644 --- a/tests/snippets/021_sprite_scale.py +++ b/tests/snippets/021_sprite_scale.py @@ -1,4 +1,4 @@ -# mcrf: objects=[Caption,Color,Scene,Sprite] verified=0.2.8-dev@a7ba486 status=ok +# mcrf: objects=[Caption,Color,Scene,Sprite] verified=0.2.8-dev status=ok # Sprite Scale - Resize sprites import mcrfpy diff --git a/tests/snippets/022_sprite_tint.py b/tests/snippets/022_sprite_tint.py index 61d8a2b..d2445af 100644 --- a/tests/snippets/022_sprite_tint.py +++ b/tests/snippets/022_sprite_tint.py @@ -1,4 +1,4 @@ -# mcrf: objects=[Caption,Color,Scene,Sprite] verified=0.2.8-dev@a7ba486 status=ok +# mcrf: objects=[Caption,Color,Scene,Sprite] verified=0.2.8-dev status=ok # Sprite Tint - Note: Sprite tinting requires shader import mcrfpy diff --git a/tests/snippets/023_grid_tiles.py b/tests/snippets/023_grid_tiles.py index c64f048..3d6e727 100644 --- a/tests/snippets/023_grid_tiles.py +++ b/tests/snippets/023_grid_tiles.py @@ -1,4 +1,4 @@ -# mcrf: objects=[Grid,Scene] verified=0.2.8-dev@a7ba486 status=ok +# mcrf: objects=[Grid,Scene] verified=0.2.8-dev status=ok # Grid Tiles - Different tile types import mcrfpy diff --git a/tests/snippets/024_grid_walkable.py b/tests/snippets/024_grid_walkable.py index 4bb6f99..647afd3 100644 --- a/tests/snippets/024_grid_walkable.py +++ b/tests/snippets/024_grid_walkable.py @@ -1,4 +1,4 @@ -# mcrf: objects=[Caption,Color,Grid,Scene] verified=0.2.8-dev@a7ba486 status=ok +# mcrf: objects=[Caption,Color,Grid,Scene] verified=0.2.8-dev status=ok # Grid Walkable - Walkable vs blocked tiles import mcrfpy diff --git a/tests/snippets/025_grid_cell_color.py b/tests/snippets/025_grid_cell_color.py index 50bb79f..f4e703c 100644 --- a/tests/snippets/025_grid_cell_color.py +++ b/tests/snippets/025_grid_cell_color.py @@ -1,4 +1,4 @@ -# mcrf: objects=[Caption,Color,ColorLayer,Grid,Scene] verified=0.2.8-dev@a7ba486 status=ok +# mcrf: objects=[Caption,Color,ColorLayer,Grid,Scene] verified=0.2.8-dev status=ok # Grid Cell Colors - Use color layer for cell colors import mcrfpy diff --git a/tests/snippets/026_grid_camera_center.py b/tests/snippets/026_grid_camera_center.py index b4ab36a..9e573d4 100644 --- a/tests/snippets/026_grid_camera_center.py +++ b/tests/snippets/026_grid_camera_center.py @@ -1,4 +1,4 @@ -# mcrf: objects=[Caption,Color,Entity,Grid,InputState,Key,Scene] verified=0.2.8-dev@a7ba486 status=ok +# mcrf: objects=[Caption,Color,Entity,Grid,InputState,Key,Scene] verified=0.2.8-dev status=ok # Grid Camera Center - Pan the view import mcrfpy diff --git a/tests/snippets/027_grid_zoom.py b/tests/snippets/027_grid_zoom.py index d5404d7..a601c71 100644 --- a/tests/snippets/027_grid_zoom.py +++ b/tests/snippets/027_grid_zoom.py @@ -1,4 +1,4 @@ -# mcrf: objects=[Caption,Color,Entity,Frame,Grid,Scene] verified=0.2.8-dev@a7ba486 status=ok +# mcrf: objects=[Caption,Color,Entity,Frame,Grid,Scene] verified=0.2.8-dev status=ok # Grid Zoom - Scale the grid view import mcrfpy diff --git a/tests/snippets/028_entity_movement.py b/tests/snippets/028_entity_movement.py index b3a09ba..fc4e53a 100644 --- a/tests/snippets/028_entity_movement.py +++ b/tests/snippets/028_entity_movement.py @@ -1,4 +1,4 @@ -# mcrf: objects=[Caption,Color,Entity,Grid,InputState,Key,Scene] verified=0.2.8-dev@a7ba486 status=ok +# mcrf: objects=[Caption,Color,Entity,Grid,InputState,Key,Scene] verified=0.2.8-dev status=ok # Entity Movement - Move entities on grid import mcrfpy diff --git a/tests/snippets/029_entity_multiple.py b/tests/snippets/029_entity_multiple.py index 8106bed..69f9065 100644 --- a/tests/snippets/029_entity_multiple.py +++ b/tests/snippets/029_entity_multiple.py @@ -1,4 +1,4 @@ -# mcrf: objects=[Caption,Color,Entity,Grid,Scene] verified=0.2.8-dev@a7ba486 status=ok +# mcrf: objects=[Caption,Color,Entity,Grid,Scene] verified=0.2.8-dev status=ok # Multiple Entities - Several entities on one grid import mcrfpy diff --git a/tests/snippets/030_entity_draw_pos.py b/tests/snippets/030_entity_draw_pos.py index 7d0b43d..a8a4e6d 100644 --- a/tests/snippets/030_entity_draw_pos.py +++ b/tests/snippets/030_entity_draw_pos.py @@ -1,4 +1,4 @@ -# mcrf: objects=[Caption,Color,Entity,Grid,Scene,Timer,Vector] verified=0.2.8-dev@a7ba486 status=ok +# mcrf: objects=[Caption,Color,Entity,Grid,Scene,Timer,Vector] verified=0.2.8-dev status=ok # Entity Draw Position - Smooth sub-tile positioning import mcrfpy diff --git a/tests/snippets/031_animation_basic.py b/tests/snippets/031_animation_basic.py index fcb83eb..a1eba49 100644 --- a/tests/snippets/031_animation_basic.py +++ b/tests/snippets/031_animation_basic.py @@ -1,4 +1,4 @@ -# mcrf: objects=[Caption,Color,Easing,Frame,InputState,Key,Scene,Timer] verified=0.2.8-dev@a7ba486 status=ok +# mcrf: objects=[Caption,Color,Easing,Frame,InputState,Key,Scene,Timer] verified=0.2.8-dev status=ok # Animation Basic - Animate position import mcrfpy diff --git a/tests/snippets/032_animation_easing_linear.py b/tests/snippets/032_animation_easing_linear.py index d9a770c..155d738 100644 --- a/tests/snippets/032_animation_easing_linear.py +++ b/tests/snippets/032_animation_easing_linear.py @@ -1,4 +1,4 @@ -# mcrf: objects=[Caption,Color,Easing,Frame,Scene,Timer] verified=0.2.8-dev@a7ba486 status=ok +# mcrf: objects=[Caption,Color,Easing,Frame,Scene,Timer] verified=0.2.8-dev status=ok # Easing: LINEAR - Constant speed import mcrfpy diff --git a/tests/snippets/033_animation_easing_ease_in.py b/tests/snippets/033_animation_easing_ease_in.py index 263b9d9..dee4c3c 100644 --- a/tests/snippets/033_animation_easing_ease_in.py +++ b/tests/snippets/033_animation_easing_ease_in.py @@ -1,4 +1,4 @@ -# mcrf: objects=[Caption,Color,Easing,Frame,Scene,Timer] verified=0.2.8-dev@a7ba486 status=ok +# mcrf: objects=[Caption,Color,Easing,Frame,Scene,Timer] verified=0.2.8-dev status=ok # Easing: EASE_IN - Slow start, fast end import mcrfpy diff --git a/tests/snippets/034_animation_easing_ease_out.py b/tests/snippets/034_animation_easing_ease_out.py index 3514aec..f413a8e 100644 --- a/tests/snippets/034_animation_easing_ease_out.py +++ b/tests/snippets/034_animation_easing_ease_out.py @@ -1,4 +1,4 @@ -# mcrf: objects=[Caption,Color,Easing,Frame,Scene,Timer] verified=0.2.8-dev@a7ba486 status=ok +# mcrf: objects=[Caption,Color,Easing,Frame,Scene,Timer] verified=0.2.8-dev status=ok # Easing: EASE_OUT - Fast start, slow end import mcrfpy diff --git a/tests/snippets/035_animation_easing_ease_in_out.py b/tests/snippets/035_animation_easing_ease_in_out.py index e808c8e..681d383 100644 --- a/tests/snippets/035_animation_easing_ease_in_out.py +++ b/tests/snippets/035_animation_easing_ease_in_out.py @@ -1,4 +1,4 @@ -# mcrf: objects=[Caption,Color,Easing,Frame,Scene,Timer] verified=0.2.8-dev@a7ba486 status=ok +# mcrf: objects=[Caption,Color,Easing,Frame,Scene,Timer] verified=0.2.8-dev status=ok # Easing: EASE_IN_OUT - Smooth both ends import mcrfpy diff --git a/tests/snippets/036_animation_easing_bounce.py b/tests/snippets/036_animation_easing_bounce.py index 563982c..84a60c3 100644 --- a/tests/snippets/036_animation_easing_bounce.py +++ b/tests/snippets/036_animation_easing_bounce.py @@ -1,4 +1,4 @@ -# mcrf: objects=[Caption,Color,Easing,Frame,Scene,Timer] verified=0.2.8-dev@a7ba486 status=ok +# mcrf: objects=[Caption,Color,Easing,Frame,Scene,Timer] verified=0.2.8-dev status=ok # Easing: BOUNCE - Bouncy effect import mcrfpy diff --git a/tests/snippets/037_animation_easing_elastic.py b/tests/snippets/037_animation_easing_elastic.py index 9412b26..8be7b63 100644 --- a/tests/snippets/037_animation_easing_elastic.py +++ b/tests/snippets/037_animation_easing_elastic.py @@ -1,4 +1,4 @@ -# mcrf: objects=[Caption,Color,Easing,Frame,Scene,Timer] verified=0.2.8-dev@a7ba486 status=ok +# mcrf: objects=[Caption,Color,Easing,Frame,Scene,Timer] verified=0.2.8-dev status=ok # Easing: ELASTIC - Springy overshoot import mcrfpy diff --git a/tests/snippets/038_animation_easing_back.py b/tests/snippets/038_animation_easing_back.py index db633ad..21472d7 100644 --- a/tests/snippets/038_animation_easing_back.py +++ b/tests/snippets/038_animation_easing_back.py @@ -1,4 +1,4 @@ -# mcrf: objects=[Caption,Color,Easing,Frame,Scene,Timer] verified=0.2.8-dev@a7ba486 status=ok +# mcrf: objects=[Caption,Color,Easing,Frame,Scene,Timer] verified=0.2.8-dev status=ok # Easing: BACK - Anticipation/overshoot import mcrfpy diff --git a/tests/snippets/039_animation_easing_comparison.py b/tests/snippets/039_animation_easing_comparison.py index ce7ba01..e665646 100644 --- a/tests/snippets/039_animation_easing_comparison.py +++ b/tests/snippets/039_animation_easing_comparison.py @@ -1,4 +1,4 @@ -# mcrf: objects=[Caption,Color,Easing,Frame,Scene,Timer] verified=0.2.8-dev@a7ba486 status=ok +# mcrf: objects=[Caption,Color,Easing,Frame,Scene,Timer] verified=0.2.8-dev status=ok # Easing Comparison - Multiple easings side by side import mcrfpy diff --git a/tests/snippets/040_animation_color.py b/tests/snippets/040_animation_color.py index 2fdcb92..14c0bf8 100644 --- a/tests/snippets/040_animation_color.py +++ b/tests/snippets/040_animation_color.py @@ -1,4 +1,4 @@ -# mcrf: objects=[Caption,Color,Easing,Frame,Scene,Timer] verified=0.2.8-dev@a7ba486 status=ok +# mcrf: objects=[Caption,Color,Easing,Frame,Scene,Timer] verified=0.2.8-dev status=ok # Animation Color - Animate fill_color import mcrfpy diff --git a/tests/snippets/041_animation_opacity.py b/tests/snippets/041_animation_opacity.py index 33b15d9..409bf43 100644 --- a/tests/snippets/041_animation_opacity.py +++ b/tests/snippets/041_animation_opacity.py @@ -1,4 +1,4 @@ -# mcrf: objects=[Caption,Color,Easing,Frame,Scene,Timer] verified=0.2.8-dev@a7ba486 status=ok +# mcrf: objects=[Caption,Color,Easing,Frame,Scene,Timer] verified=0.2.8-dev status=ok # Animation Opacity - Fade in/out import mcrfpy diff --git a/tests/snippets/042_animation_size.py b/tests/snippets/042_animation_size.py index 304a469..6c2d739 100644 --- a/tests/snippets/042_animation_size.py +++ b/tests/snippets/042_animation_size.py @@ -1,4 +1,4 @@ -# mcrf: objects=[Caption,Color,Easing,Frame,Scene,Timer] verified=0.2.8-dev@a7ba486 status=ok +# mcrf: objects=[Caption,Color,Easing,Frame,Scene,Timer] verified=0.2.8-dev status=ok # Animation Size - Grow and shrink import mcrfpy diff --git a/tests/snippets/043_animation_sprite_index.py b/tests/snippets/043_animation_sprite_index.py index 73bb596..4d8570e 100644 --- a/tests/snippets/043_animation_sprite_index.py +++ b/tests/snippets/043_animation_sprite_index.py @@ -1,4 +1,4 @@ -# mcrf: objects=[Caption,Color,Easing,Frame,Scene,Sprite,Timer] verified=0.2.8-dev@a7ba486 status=ok +# mcrf: objects=[Caption,Color,Easing,Frame,Scene,Sprite,Timer] verified=0.2.8-dev status=ok # Animation Sprite Index - Sprite animation import mcrfpy diff --git a/tests/snippets/044_animation_callback.py b/tests/snippets/044_animation_callback.py index c001869..936eb0e 100644 --- a/tests/snippets/044_animation_callback.py +++ b/tests/snippets/044_animation_callback.py @@ -1,4 +1,4 @@ -# mcrf: objects=[Caption,Color,Easing,Frame,Scene,Timer] verified=0.2.8-dev@a7ba486 status=ok +# mcrf: objects=[Caption,Color,Easing,Frame,Scene,Timer] verified=0.2.8-dev status=ok # Animation Callback - Run code when animation ends import mcrfpy diff --git a/tests/snippets/045_animation_chain.py b/tests/snippets/045_animation_chain.py index 2b3dabd..c7b201c 100644 --- a/tests/snippets/045_animation_chain.py +++ b/tests/snippets/045_animation_chain.py @@ -1,4 +1,4 @@ -# mcrf: objects=[Caption,Color,Easing,Frame,Scene,Timer] verified=0.2.8-dev@a7ba486 status=ok +# mcrf: objects=[Caption,Color,Easing,Frame,Scene,Timer] verified=0.2.8-dev status=ok # Animation Chain - Sequential animations import mcrfpy diff --git a/tests/snippets/046_timer_basic.py b/tests/snippets/046_timer_basic.py index 0986244..3c5d0d2 100644 --- a/tests/snippets/046_timer_basic.py +++ b/tests/snippets/046_timer_basic.py @@ -1,4 +1,4 @@ -# mcrf: objects=[Caption,Color,Frame,Scene,Timer] verified=0.2.8-dev@a7ba486 status=ok +# mcrf: objects=[Caption,Color,Frame,Scene,Timer] verified=0.2.8-dev status=ok # Timer Basic - Periodic callback import mcrfpy diff --git a/tests/snippets/047_timer_once.py b/tests/snippets/047_timer_once.py index 144ca49..4f6626d 100644 --- a/tests/snippets/047_timer_once.py +++ b/tests/snippets/047_timer_once.py @@ -1,4 +1,4 @@ -# mcrf: objects=[Caption,Color,Frame,Scene,Timer] verified=0.2.8-dev@a7ba486 status=ok +# mcrf: objects=[Caption,Color,Frame,Scene,Timer] verified=0.2.8-dev status=ok # Timer Once - Single-shot timer import mcrfpy diff --git a/tests/snippets/048_timer_control.py b/tests/snippets/048_timer_control.py index bf91526..f2e3901 100644 --- a/tests/snippets/048_timer_control.py +++ b/tests/snippets/048_timer_control.py @@ -1,4 +1,4 @@ -# mcrf: objects=[Caption,Color,Frame,InputState,Key,Scene,Timer] verified=0.2.8-dev@a7ba486 status=ok +# mcrf: objects=[Caption,Color,Frame,InputState,Key,Scene,Timer] verified=0.2.8-dev status=ok # Timer Control - Pause, resume, stop import mcrfpy diff --git a/tests/snippets/049_input_keyboard.py b/tests/snippets/049_input_keyboard.py index e8c9374..707425a 100644 --- a/tests/snippets/049_input_keyboard.py +++ b/tests/snippets/049_input_keyboard.py @@ -1,4 +1,4 @@ -# mcrf: objects=[Caption,Color,Frame,InputState,Scene] verified=0.2.8-dev@a7ba486 status=ok +# mcrf: objects=[Caption,Color,Frame,InputState,Scene] verified=0.2.8-dev status=ok # Input Keyboard - Key press handling import mcrfpy diff --git a/tests/snippets/050_input_mouse_click.py b/tests/snippets/050_input_mouse_click.py index 6b18840..fb772b4 100644 --- a/tests/snippets/050_input_mouse_click.py +++ b/tests/snippets/050_input_mouse_click.py @@ -1,4 +1,4 @@ -# mcrf: objects=[Caption,Color,Frame,InputState,Scene] verified=0.2.8-dev@a7ba486 status=ok +# mcrf: objects=[Caption,Color,Frame,InputState,Scene] verified=0.2.8-dev status=ok # Input Mouse Click - Click on frames import mcrfpy diff --git a/tests/snippets/051_input_mouse_hover.py b/tests/snippets/051_input_mouse_hover.py index 589abff..8de0002 100644 --- a/tests/snippets/051_input_mouse_hover.py +++ b/tests/snippets/051_input_mouse_hover.py @@ -1,4 +1,4 @@ -# mcrf: objects=[Caption,Color,Frame,Scene] verified=0.2.8-dev@a7ba486 status=ok +# mcrf: objects=[Caption,Color,Frame,Scene] verified=0.2.8-dev status=ok # Input Mouse Hover - Enter/exit/move events import mcrfpy diff --git a/tests/snippets/052_input_grid_cell_click.py b/tests/snippets/052_input_grid_cell_click.py index b2fe014..4909e7a 100644 --- a/tests/snippets/052_input_grid_cell_click.py +++ b/tests/snippets/052_input_grid_cell_click.py @@ -1,4 +1,4 @@ -# mcrf: objects=[Caption,Color,ColorLayer,Grid,InputState,Scene] verified=0.2.8-dev@a7ba486 status=ok +# mcrf: objects=[Caption,Color,ColorLayer,Grid,InputState,Scene] verified=0.2.8-dev status=ok # Input Grid Cell Click - Click on grid cells import mcrfpy diff --git a/tests/snippets/053_input_grid_cell_hover.py b/tests/snippets/053_input_grid_cell_hover.py index 2e3063d..8055e4e 100644 --- a/tests/snippets/053_input_grid_cell_hover.py +++ b/tests/snippets/053_input_grid_cell_hover.py @@ -1,4 +1,4 @@ -# mcrf: objects=[Caption,Color,ColorLayer,Grid,Scene] verified=0.2.8-dev@a7ba486 status=ok +# mcrf: objects=[Caption,Color,ColorLayer,Grid,Scene] verified=0.2.8-dev status=ok # Input Grid Cell Hover - Hover over grid cells import mcrfpy diff --git a/tests/snippets/054_pathfinding_astar.py b/tests/snippets/054_pathfinding_astar.py index 5508227..114ddf7 100644 --- a/tests/snippets/054_pathfinding_astar.py +++ b/tests/snippets/054_pathfinding_astar.py @@ -1,4 +1,4 @@ -# mcrf: objects=[Caption,Color,ColorLayer,Grid,Scene] verified=0.2.8-dev@a7ba486 status=ok +# mcrf: objects=[Caption,Color,ColorLayer,Grid,Scene] verified=0.2.8-dev status=ok # Pathfinding A* - Find path between points import mcrfpy diff --git a/tests/snippets/055_pathfinding_entity.py b/tests/snippets/055_pathfinding_entity.py index 53ad73d..564db07 100644 --- a/tests/snippets/055_pathfinding_entity.py +++ b/tests/snippets/055_pathfinding_entity.py @@ -1,4 +1,4 @@ -# mcrf: objects=[Caption,Color,ColorLayer,Entity,Grid,Scene] verified=0.2.8-dev@a7ba486 status=ok +# mcrf: objects=[Caption,Color,ColorLayer,Entity,Grid,Scene] verified=0.2.8-dev status=ok # Entity Pathfinding - Entity path_to method import mcrfpy diff --git a/tests/snippets/056_pathfinding_dijkstra.py b/tests/snippets/056_pathfinding_dijkstra.py index 0895aa9..0745b86 100644 --- a/tests/snippets/056_pathfinding_dijkstra.py +++ b/tests/snippets/056_pathfinding_dijkstra.py @@ -1,4 +1,4 @@ -# mcrf: objects=[Caption,Color,ColorLayer,Grid,Scene] verified=0.2.8-dev@a7ba486 status=ok +# mcrf: objects=[Caption,Color,ColorLayer,Grid,Scene] verified=0.2.8-dev status=ok # Dijkstra Map - Distance-based pathfinding import mcrfpy diff --git a/tests/snippets/057_fov_basic.py b/tests/snippets/057_fov_basic.py index d84bc89..d46a0e0 100644 --- a/tests/snippets/057_fov_basic.py +++ b/tests/snippets/057_fov_basic.py @@ -1,4 +1,4 @@ -# mcrf: objects=[Caption,Color,ColorLayer,Entity,Grid,Scene] verified=0.2.8-dev@a7ba486 status=ok +# mcrf: objects=[Caption,Color,ColorLayer,Entity,Grid,Scene] verified=0.2.8-dev status=ok # FOV Basic - Field of view computation import mcrfpy diff --git a/tests/snippets/058_fov_algorithms.py b/tests/snippets/058_fov_algorithms.py index cd8b5ac..6a51cae 100644 --- a/tests/snippets/058_fov_algorithms.py +++ b/tests/snippets/058_fov_algorithms.py @@ -1,4 +1,4 @@ -# mcrf: objects=[Caption,Color,ColorLayer,FOV,Frame,Grid,Scene] verified=0.2.8-dev@a7ba486 status=ok +# mcrf: objects=[Caption,Color,ColorLayer,FOV,Frame,Grid,Scene] verified=0.2.8-dev status=ok # FOV Algorithms - Compare different algorithms import mcrfpy diff --git a/tests/snippets/059_fov_perspective.py b/tests/snippets/059_fov_perspective.py index 5d9785f..e8cb6cc 100644 --- a/tests/snippets/059_fov_perspective.py +++ b/tests/snippets/059_fov_perspective.py @@ -1,4 +1,4 @@ -# mcrf: objects=[Caption,Color,Entity,Grid,InputState,Key,Scene] verified=0.2.8-dev@a7ba486 status=ok +# mcrf: objects=[Caption,Color,Entity,Grid,InputState,Key,Scene] verified=0.2.8-dev status=ok # FOV Perspective Entity - View from entity's perspective import mcrfpy diff --git a/tests/snippets/060_bsp_basic.py b/tests/snippets/060_bsp_basic.py index 1c28468..a13761f 100644 --- a/tests/snippets/060_bsp_basic.py +++ b/tests/snippets/060_bsp_basic.py @@ -1,4 +1,4 @@ -# mcrf: objects=[BSP,Caption,Color,ColorLayer,Grid,Scene] verified=0.2.8-dev@a7ba486 status=ok +# mcrf: objects=[BSP,Caption,Color,ColorLayer,Grid,Scene] verified=0.2.8-dev status=ok # BSP Basic - Binary space partition for dungeon generation import mcrfpy diff --git a/tests/snippets/061_scene_switching.py b/tests/snippets/061_scene_switching.py index b69525f..ca04f48 100644 --- a/tests/snippets/061_scene_switching.py +++ b/tests/snippets/061_scene_switching.py @@ -1,4 +1,4 @@ -# mcrf: objects=[Caption,Color,Frame,InputState,Key,Scene] verified=0.2.8-dev@a7ba486 status=ok +# mcrf: objects=[Caption,Color,Frame,InputState,Key,Scene] verified=0.2.8-dev status=ok # Scene Switching - Multiple scenes import mcrfpy diff --git a/tests/snippets/062_scene_transition_fade.py b/tests/snippets/062_scene_transition_fade.py index c28a3e7..8e29583 100644 --- a/tests/snippets/062_scene_transition_fade.py +++ b/tests/snippets/062_scene_transition_fade.py @@ -1,4 +1,4 @@ -# mcrf: objects=[Caption,Color,Frame,InputState,Key,Scene,Transition] verified=0.2.8-dev@a7ba486 status=ok +# mcrf: objects=[Caption,Color,Frame,InputState,Key,Scene,Transition] verified=0.2.8-dev status=ok # Scene Transition Fade - Smooth scene transitions import mcrfpy diff --git a/tests/snippets/063_scene_transition_slide.py b/tests/snippets/063_scene_transition_slide.py index db0f14e..ea86f3b 100644 --- a/tests/snippets/063_scene_transition_slide.py +++ b/tests/snippets/063_scene_transition_slide.py @@ -1,4 +1,4 @@ -# mcrf: objects=[Caption,Color,Frame,InputState,Key,Scene,Transition] verified=0.2.8-dev@a7ba486 status=ok +# mcrf: objects=[Caption,Color,Frame,InputState,Key,Scene,Transition] verified=0.2.8-dev status=ok # Scene Transition Slide - Sliding transitions import mcrfpy diff --git a/tests/snippets/064_vector_basics.py b/tests/snippets/064_vector_basics.py index fd6d0fc..a59f3da 100644 --- a/tests/snippets/064_vector_basics.py +++ b/tests/snippets/064_vector_basics.py @@ -1,4 +1,4 @@ -# mcrf: objects=[Caption,Color,Frame,Scene,Vector] verified=0.2.8-dev@a7ba486 status=ok +# mcrf: objects=[Caption,Color,Frame,Scene,Vector] verified=0.2.8-dev status=ok # Vector Basics - 2D vector operations import mcrfpy diff --git a/tests/snippets/065_color_components.py b/tests/snippets/065_color_components.py index bb7760f..54cc532 100644 --- a/tests/snippets/065_color_components.py +++ b/tests/snippets/065_color_components.py @@ -1,4 +1,4 @@ -# mcrf: objects=[Caption,Color,Frame,Scene] verified=0.2.8-dev@a7ba486 status=ok +# mcrf: objects=[Caption,Color,Frame,Scene] verified=0.2.8-dev status=ok # Color Components - RGBA color manipulation import mcrfpy diff --git a/tests/snippets/066_find_elements.py b/tests/snippets/066_find_elements.py index 80f5f13..641ede2 100644 --- a/tests/snippets/066_find_elements.py +++ b/tests/snippets/066_find_elements.py @@ -1,4 +1,4 @@ -# mcrf: objects=[Caption,Color,Frame,Scene] verified=0.2.8-dev@a7ba486 status=ok +# mcrf: objects=[Caption,Color,Frame,Scene] verified=0.2.8-dev status=ok # Find Elements - Locate UI by name import mcrfpy diff --git a/tests/snippets/067_collection_iteration.py b/tests/snippets/067_collection_iteration.py index 6c21071..bd16f01 100644 --- a/tests/snippets/067_collection_iteration.py +++ b/tests/snippets/067_collection_iteration.py @@ -1,4 +1,4 @@ -# mcrf: objects=[Caption,Color,Frame,Scene] verified=0.2.8-dev@a7ba486 status=ok +# mcrf: objects=[Caption,Color,Frame,Scene] verified=0.2.8-dev status=ok # Collection Iteration - Loop through UI children import mcrfpy diff --git a/tests/snippets/068_entity_collection.py b/tests/snippets/068_entity_collection.py index 9a8ce05..32e6bf2 100644 --- a/tests/snippets/068_entity_collection.py +++ b/tests/snippets/068_entity_collection.py @@ -1,4 +1,4 @@ -# mcrf: objects=[Caption,Color,Entity,Grid,Scene] verified=0.2.8-dev@a7ba486 status=ok +# mcrf: objects=[Caption,Color,Entity,Grid,Scene] verified=0.2.8-dev status=ok # Entity Collection - Manage grid entities import mcrfpy diff --git a/tests/snippets/069_window_properties.py b/tests/snippets/069_window_properties.py index 3602b9b..cf93579 100644 --- a/tests/snippets/069_window_properties.py +++ b/tests/snippets/069_window_properties.py @@ -1,4 +1,4 @@ -# mcrf: objects=[Caption,Color,Frame,Scene] verified=0.2.8-dev@a7ba486 status=ok +# mcrf: objects=[Caption,Color,Frame,Scene] verified=0.2.8-dev status=ok # Window Properties - Access window settings import mcrfpy diff --git a/tests/snippets/070_keyboard_state.py b/tests/snippets/070_keyboard_state.py index 8e78c80..4ee3915 100644 --- a/tests/snippets/070_keyboard_state.py +++ b/tests/snippets/070_keyboard_state.py @@ -1,4 +1,4 @@ -# mcrf: objects=[Caption,Color,Frame,InputState,Scene] verified=0.2.8-dev@a7ba486 status=ok +# mcrf: objects=[Caption,Color,Frame,InputState,Scene] verified=0.2.8-dev status=ok # Keyboard State - Track key presses import mcrfpy diff --git a/tests/snippets/071_mouse_state.py b/tests/snippets/071_mouse_state.py index 6f8315a..86e1277 100644 --- a/tests/snippets/071_mouse_state.py +++ b/tests/snippets/071_mouse_state.py @@ -1,4 +1,4 @@ -# mcrf: objects=[Caption,Color,Frame,Scene,Timer,Vector] verified=0.2.8-dev@a7ba486 status=ok +# mcrf: objects=[Caption,Color,Frame,Scene,Timer,Vector] verified=0.2.8-dev status=ok # Mouse State - Track mouse position and buttons import mcrfpy diff --git a/tests/snippets/072_ui_button.py b/tests/snippets/072_ui_button.py index 4de63e3..79a0aa0 100644 --- a/tests/snippets/072_ui_button.py +++ b/tests/snippets/072_ui_button.py @@ -1,4 +1,4 @@ -# mcrf: objects=[Caption,Color,Frame,InputState,Scene] verified=0.2.8-dev@a7ba486 status=ok +# mcrf: objects=[Caption,Color,Frame,InputState,Scene] verified=0.2.8-dev status=ok # UI Button - Interactive button with hover import mcrfpy diff --git a/tests/snippets/073_ui_health_bar.py b/tests/snippets/073_ui_health_bar.py index 8d4290d..8051965 100644 --- a/tests/snippets/073_ui_health_bar.py +++ b/tests/snippets/073_ui_health_bar.py @@ -1,4 +1,4 @@ -# mcrf: objects=[Caption,Color,Easing,Frame,InputState,Key,Scene] verified=0.2.8-dev@a7ba486 status=ok +# mcrf: objects=[Caption,Color,Easing,Frame,InputState,Key,Scene] verified=0.2.8-dev status=ok # UI Health Bar - Animated health display import mcrfpy diff --git a/tests/snippets/074_ui_tooltip.py b/tests/snippets/074_ui_tooltip.py index 7270ec5..2e67d3d 100644 --- a/tests/snippets/074_ui_tooltip.py +++ b/tests/snippets/074_ui_tooltip.py @@ -1,4 +1,4 @@ -# mcrf: objects=[Caption,Color,Frame,Scene,Vector] verified=0.2.8-dev@a7ba486 status=ok +# mcrf: objects=[Caption,Color,Frame,Scene,Vector] verified=0.2.8-dev status=ok # UI Tooltip - Hover tooltip display import mcrfpy diff --git a/tests/snippets/075_ui_message_log.py b/tests/snippets/075_ui_message_log.py index 65fb54f..ea26fe6 100644 --- a/tests/snippets/075_ui_message_log.py +++ b/tests/snippets/075_ui_message_log.py @@ -1,4 +1,4 @@ -# mcrf: objects=[Caption,Color,Frame,Scene,Timer] verified=0.2.8-dev@a7ba486 status=ok +# mcrf: objects=[Caption,Color,Frame,Scene,Timer] verified=0.2.8-dev status=ok # UI Message Log - Scrolling text messages import mcrfpy diff --git a/tests/snippets/076_effect_screen_shake.py b/tests/snippets/076_effect_screen_shake.py index e71edfe..ac7fb08 100644 --- a/tests/snippets/076_effect_screen_shake.py +++ b/tests/snippets/076_effect_screen_shake.py @@ -1,4 +1,4 @@ -# mcrf: objects=[Caption,Color,Frame,InputState,Key,Scene,Timer] verified=0.2.8-dev@a7ba486 status=ok +# mcrf: objects=[Caption,Color,Frame,InputState,Key,Scene,Timer] verified=0.2.8-dev status=ok # Effect Screen Shake - Impact feedback import mcrfpy import random diff --git a/tests/snippets/077_effect_damage_flash.py b/tests/snippets/077_effect_damage_flash.py index 2ff4884..4956e02 100644 --- a/tests/snippets/077_effect_damage_flash.py +++ b/tests/snippets/077_effect_damage_flash.py @@ -1,4 +1,4 @@ -# mcrf: objects=[Caption,Color,Easing,Frame,InputState,Key,Scene,Sprite,Timer] verified=0.2.8-dev@a7ba486 status=ok +# mcrf: objects=[Caption,Color,Easing,Frame,InputState,Key,Scene,Sprite,Timer] verified=0.2.8-dev status=ok # Effect Damage Flash - Hit feedback on frame import mcrfpy diff --git a/tests/snippets/078_effect_floating_text.py b/tests/snippets/078_effect_floating_text.py index 70c8097..b73e04e 100644 --- a/tests/snippets/078_effect_floating_text.py +++ b/tests/snippets/078_effect_floating_text.py @@ -1,4 +1,4 @@ -# mcrf: objects=[Caption,Color,Easing,Frame,InputState,Scene,Timer] verified=0.2.8-dev@a7ba486 status=ok +# mcrf: objects=[Caption,Color,Easing,Frame,InputState,Scene,Timer] verified=0.2.8-dev status=ok # Effect Floating Text - Damage numbers import mcrfpy import random diff --git a/tests/snippets/079_effect_pulse.py b/tests/snippets/079_effect_pulse.py index 5d5291f..7f2ac77 100644 --- a/tests/snippets/079_effect_pulse.py +++ b/tests/snippets/079_effect_pulse.py @@ -1,4 +1,4 @@ -# mcrf: objects=[Caption,Color,Frame,Scene,Timer] verified=0.2.8-dev@a7ba486 status=ok +# mcrf: objects=[Caption,Color,Frame,Scene,Timer] verified=0.2.8-dev status=ok # Effect Pulse - Pulsing highlight import mcrfpy import math diff --git a/tests/snippets/080_game_turn_based.py b/tests/snippets/080_game_turn_based.py index c9c6efe..50d98a7 100644 --- a/tests/snippets/080_game_turn_based.py +++ b/tests/snippets/080_game_turn_based.py @@ -1,4 +1,4 @@ -# mcrf: objects=[Caption,Color,Entity,Grid,InputState,Key,Scene,Timer] verified=0.2.8-dev@a7ba486 status=ok +# mcrf: objects=[Caption,Color,Entity,Grid,InputState,Key,Scene,Timer] verified=0.2.8-dev status=ok # Game Turn-Based - Simple turn system import mcrfpy diff --git a/tests/snippets/081_game_inventory.py b/tests/snippets/081_game_inventory.py index 4b332f5..a4af5e0 100644 --- a/tests/snippets/081_game_inventory.py +++ b/tests/snippets/081_game_inventory.py @@ -1,4 +1,4 @@ -# mcrf: objects=[Caption,Color,Frame,Scene,Sprite] verified=0.2.8-dev@a7ba486 status=ok +# mcrf: objects=[Caption,Color,Frame,Scene,Sprite] verified=0.2.8-dev status=ok # Game Inventory - Grid-based inventory UI import mcrfpy diff --git a/tests/snippets/082_game_minimap.py b/tests/snippets/082_game_minimap.py index 2b8cfcc..a9356aa 100644 --- a/tests/snippets/082_game_minimap.py +++ b/tests/snippets/082_game_minimap.py @@ -1,4 +1,4 @@ -# mcrf: objects=[Caption,Color,Entity,Grid,InputState,Key,Scene] verified=0.2.8-dev@a7ba486 status=ok +# mcrf: objects=[Caption,Color,Entity,Grid,InputState,Key,Scene] verified=0.2.8-dev status=ok # Game Minimap - Small overview map import mcrfpy diff --git a/tests/snippets/083_grid_layers.py b/tests/snippets/083_grid_layers.py index 9fe60ed..eb63763 100644 --- a/tests/snippets/083_grid_layers.py +++ b/tests/snippets/083_grid_layers.py @@ -1,4 +1,4 @@ -# mcrf: objects=[Caption,Color,ColorLayer,Grid,Scene,TileLayer] verified=0.2.8-dev@a7ba486 status=ok +# mcrf: objects=[Caption,Color,ColorLayer,Grid,Scene,TileLayer] verified=0.2.8-dev status=ok # Grid Layers - Multiple tile layers import mcrfpy diff --git a/tests/snippets/084_bresenham_line.py b/tests/snippets/084_bresenham_line.py index c80c887..6cad4af 100644 --- a/tests/snippets/084_bresenham_line.py +++ b/tests/snippets/084_bresenham_line.py @@ -1,4 +1,4 @@ -# mcrf: objects=[Caption,Color,ColorLayer,Grid,Scene] verified=0.2.8-dev@a7ba486 status=ok +# mcrf: objects=[Caption,Color,ColorLayer,Grid,Scene] verified=0.2.8-dev status=ok # Bresenham Line - Draw lines on grid import mcrfpy diff --git a/tests/snippets/085_entity_visibility.py b/tests/snippets/085_entity_visibility.py index 9ebe030..d339d57 100644 --- a/tests/snippets/085_entity_visibility.py +++ b/tests/snippets/085_entity_visibility.py @@ -1,4 +1,4 @@ -# mcrf: objects=[Caption,Color,Entity,Grid,Scene,Timer] verified=0.2.8-dev@a7ba486 status=ok +# mcrf: objects=[Caption,Color,Entity,Grid,Scene,Timer] verified=0.2.8-dev status=ok # Entity Visibility - Check what entities can see import mcrfpy diff --git a/tests/snippets/086_procgen_cellular.py b/tests/snippets/086_procgen_cellular.py index 1c1ac22..243f1e4 100644 --- a/tests/snippets/086_procgen_cellular.py +++ b/tests/snippets/086_procgen_cellular.py @@ -1,4 +1,4 @@ -# mcrf: objects=[Caption,Grid,Scene] verified=0.2.8-dev@a7ba486 status=ok +# mcrf: objects=[Caption,Grid,Scene] verified=0.2.8-dev status=ok # Procgen Cellular - Cave generation import mcrfpy import random diff --git a/tests/snippets/087_grid_point_state.py b/tests/snippets/087_grid_point_state.py index 9c13966..cc756cb 100644 --- a/tests/snippets/087_grid_point_state.py +++ b/tests/snippets/087_grid_point_state.py @@ -1,4 +1,4 @@ -# mcrf: objects=[Caption,Color,ColorLayer,Grid,InputState,Scene] verified=0.2.8-dev@a7ba486 status=ok +# mcrf: objects=[Caption,Color,ColorLayer,Grid,InputState,Scene] verified=0.2.8-dev status=ok # Grid Point State - Cell properties import mcrfpy diff --git a/tests/snippets/088_animation_multiple.py b/tests/snippets/088_animation_multiple.py index 19b55ce..429f072 100644 --- a/tests/snippets/088_animation_multiple.py +++ b/tests/snippets/088_animation_multiple.py @@ -1,4 +1,4 @@ -# mcrf: objects=[Caption,Color,Easing,Frame,Scene,Timer] verified=0.2.8-dev@a7ba486 status=ok +# mcrf: objects=[Caption,Color,Easing,Frame,Scene,Timer] verified=0.2.8-dev status=ok # Animation Multiple - Several animations at once import mcrfpy diff --git a/tests/snippets/089_camera_follow.py b/tests/snippets/089_camera_follow.py index 0e67c54..031ea38 100644 --- a/tests/snippets/089_camera_follow.py +++ b/tests/snippets/089_camera_follow.py @@ -1,4 +1,4 @@ -# mcrf: objects=[Caption,Color,Easing,Entity,Grid,InputState,Key,Scene] verified=0.2.8-dev@a7ba486 status=ok +# mcrf: objects=[Caption,Color,Easing,Entity,Grid,InputState,Key,Scene] verified=0.2.8-dev status=ok # Camera Follow - Camera tracks entity import mcrfpy diff --git a/tests/snippets/090_scene_lifecycle.py b/tests/snippets/090_scene_lifecycle.py index e0e850a..55c258e 100644 --- a/tests/snippets/090_scene_lifecycle.py +++ b/tests/snippets/090_scene_lifecycle.py @@ -1,4 +1,4 @@ -# mcrf: objects=[Caption,Color,Frame,InputState,Key,Scene] verified=0.2.8-dev@a7ba486 status=ok +# mcrf: objects=[Caption,Color,Frame,InputState,Key,Scene] verified=0.2.8-dev status=ok # Scene Lifecycle - on_enter/on_exit callbacks import mcrfpy diff --git a/tests/snippets/091_metrics_display.py b/tests/snippets/091_metrics_display.py index 9fe033d..bce1ddd 100644 --- a/tests/snippets/091_metrics_display.py +++ b/tests/snippets/091_metrics_display.py @@ -1,4 +1,4 @@ -# mcrf: objects=[Caption,Color,Frame,Scene,Timer] verified=0.2.8-dev@a7ba486 status=ok +# mcrf: objects=[Caption,Color,Frame,Scene,Timer] verified=0.2.8-dev status=ok # Metrics Display - Performance monitoring import mcrfpy diff --git a/tests/snippets/092_global_pos.py b/tests/snippets/092_global_pos.py index f5a73eb..9d471de 100644 --- a/tests/snippets/092_global_pos.py +++ b/tests/snippets/092_global_pos.py @@ -1,4 +1,4 @@ -# mcrf: objects=[Caption,Color,Frame,Scene] verified=0.2.8-dev@a7ba486 status=ok +# mcrf: objects=[Caption,Color,Frame,Scene] verified=0.2.8-dev status=ok # Global Position - World coordinates import mcrfpy diff --git a/tests/snippets/093_parent_reference.py b/tests/snippets/093_parent_reference.py index 2c97e8c..42c9c6e 100644 --- a/tests/snippets/093_parent_reference.py +++ b/tests/snippets/093_parent_reference.py @@ -1,4 +1,4 @@ -# mcrf: objects=[Caption,Color,Frame,InputState,Scene] verified=0.2.8-dev@a7ba486 status=ok +# mcrf: objects=[Caption,Color,Frame,InputState,Scene] verified=0.2.8-dev status=ok # Parent Reference - Access parent from child import mcrfpy diff --git a/tests/snippets/094_entity_die.py b/tests/snippets/094_entity_die.py index 871463a..64e3205 100644 --- a/tests/snippets/094_entity_die.py +++ b/tests/snippets/094_entity_die.py @@ -1,4 +1,4 @@ -# mcrf: objects=[Caption,Color,Entity,Grid,InputState,Key,Scene] verified=0.2.8-dev@a7ba486 status=ok +# mcrf: objects=[Caption,Color,Entity,Grid,InputState,Key,Scene] verified=0.2.8-dev status=ok # Entity Die - Remove entity from grid import mcrfpy diff --git a/tests/snippets/095_entity_index.py b/tests/snippets/095_entity_index.py index f4f2402..dff9630 100644 --- a/tests/snippets/095_entity_index.py +++ b/tests/snippets/095_entity_index.py @@ -1,4 +1,4 @@ -# mcrf: objects=[Caption,Color,Entity,Grid,InputState,Scene] verified=0.2.8-dev@a7ba486 status=ok +# mcrf: objects=[Caption,Color,Entity,Grid,InputState,Scene] verified=0.2.8-dev status=ok # Entity Index - Get entity position in collection import mcrfpy diff --git a/tests/snippets/096_timer_remaining.py b/tests/snippets/096_timer_remaining.py index 056eda3..48512d8 100644 --- a/tests/snippets/096_timer_remaining.py +++ b/tests/snippets/096_timer_remaining.py @@ -1,4 +1,4 @@ -# mcrf: objects=[Caption,Color,Frame,Scene,Timer] verified=0.2.8-dev@a7ba486 status=ok +# mcrf: objects=[Caption,Color,Frame,Scene,Timer] verified=0.2.8-dev status=ok # Timer Remaining - Check time until next tick import mcrfpy diff --git a/tests/snippets/097_hovered_state.py b/tests/snippets/097_hovered_state.py index c404ce4..712e78a 100644 --- a/tests/snippets/097_hovered_state.py +++ b/tests/snippets/097_hovered_state.py @@ -1,4 +1,4 @@ -# mcrf: objects=[Caption,Color,Frame,Scene,Timer] verified=0.2.8-dev@a7ba486 status=ok +# mcrf: objects=[Caption,Color,Frame,Scene,Timer] verified=0.2.8-dev status=ok # Hovered State - Check if element is hovered import mcrfpy diff --git a/tests/snippets/098_grid_fill_color.py b/tests/snippets/098_grid_fill_color.py index 82ac5fe..05784cc 100644 --- a/tests/snippets/098_grid_fill_color.py +++ b/tests/snippets/098_grid_fill_color.py @@ -1,4 +1,4 @@ -# mcrf: objects=[Caption,Color,Entity,Frame,Grid,Scene] verified=0.2.8-dev@a7ba486 status=ok +# mcrf: objects=[Caption,Color,Entity,Frame,Grid,Scene] verified=0.2.8-dev status=ok # Grid Fill Color - Background color import mcrfpy diff --git a/tests/snippets/099_scene_pos.py b/tests/snippets/099_scene_pos.py index a90258b..dc90bc1 100644 --- a/tests/snippets/099_scene_pos.py +++ b/tests/snippets/099_scene_pos.py @@ -1,4 +1,4 @@ -# mcrf: objects=[Caption,Color,Frame,InputState,Key,Scene,Vector] verified=0.2.8-dev@a7ba486 status=ok +# mcrf: objects=[Caption,Color,Frame,InputState,Key,Scene,Vector] verified=0.2.8-dev status=ok # Scene Position - Offset all scene content import mcrfpy diff --git a/tests/snippets/100_complete_game.py b/tests/snippets/100_complete_game.py index 314b4f2..bc326ee 100644 --- a/tests/snippets/100_complete_game.py +++ b/tests/snippets/100_complete_game.py @@ -1,4 +1,4 @@ -# mcrf: objects=[Caption,Color,Entity,Frame,Grid,InputState,Key,Scene] verified=0.2.8-dev@a7ba486 status=ok +# mcrf: objects=[Caption,Color,Entity,Frame,Grid,InputState,Key,Scene] verified=0.2.8-dev status=ok # Complete Mini-Game - Putting it all together import mcrfpy import random diff --git a/tests/snippets/101_grid_camera_rotation.py b/tests/snippets/101_grid_camera_rotation.py index 4357286..6a56dc3 100644 --- a/tests/snippets/101_grid_camera_rotation.py +++ b/tests/snippets/101_grid_camera_rotation.py @@ -1,4 +1,4 @@ -# mcrf: objects=[Caption,Color,Entity,Grid,InputState,Key,Scene] verified=0.2.8-dev@a7ba486 status=ok +# mcrf: objects=[Caption,Color,Entity,Grid,InputState,Key,Scene] verified=0.2.8-dev status=ok # Grid Camera Rotation - Rotate the view import mcrfpy diff --git a/tests/snippets/102_bsp_traversal.py b/tests/snippets/102_bsp_traversal.py index 604c5a8..167df8e 100644 --- a/tests/snippets/102_bsp_traversal.py +++ b/tests/snippets/102_bsp_traversal.py @@ -1,4 +1,4 @@ -# mcrf: objects=[BSP,Caption,Color,ColorLayer,Frame,Grid,Scene,Traversal] verified=0.2.8-dev@a7ba486 status=ok +# mcrf: objects=[BSP,Caption,Color,ColorLayer,Frame,Grid,Scene,Traversal] verified=0.2.8-dev status=ok # BSP Traversal - Different traversal orders import mcrfpy diff --git a/tests/snippets/103_animation_pos_vector.py b/tests/snippets/103_animation_pos_vector.py index f18445a..42f02ad 100644 --- a/tests/snippets/103_animation_pos_vector.py +++ b/tests/snippets/103_animation_pos_vector.py @@ -1,4 +1,4 @@ -# mcrf: objects=[Caption,Color,Easing,Frame,Scene,Timer] verified=0.2.8-dev@a7ba486 status=ok +# mcrf: objects=[Caption,Color,Easing,Frame,Scene,Timer] verified=0.2.8-dev status=ok # Animation Position - Animate x and y along a path import mcrfpy diff --git a/tests/snippets/104_texture_properties.py b/tests/snippets/104_texture_properties.py index 68de4c7..95491b1 100644 --- a/tests/snippets/104_texture_properties.py +++ b/tests/snippets/104_texture_properties.py @@ -1,4 +1,4 @@ -# mcrf: objects=[Caption,Color,Frame,Scene,Sprite] verified=0.2.8-dev@a7ba486 status=ok +# mcrf: objects=[Caption,Color,Frame,Scene,Sprite] verified=0.2.8-dev status=ok # Texture Properties - Texture information import mcrfpy diff --git a/tests/snippets/105_scenes_dict.py b/tests/snippets/105_scenes_dict.py index 118344e..a2b4c8a 100644 --- a/tests/snippets/105_scenes_dict.py +++ b/tests/snippets/105_scenes_dict.py @@ -1,4 +1,4 @@ -# mcrf: objects=[Caption,Color,Frame,Scene] verified=0.2.8-dev@a7ba486 status=ok +# mcrf: objects=[Caption,Color,Frame,Scene] verified=0.2.8-dev status=ok # Scenes Tuple - Access all scenes import mcrfpy diff --git a/tests/snippets/106_timers_list.py b/tests/snippets/106_timers_list.py index c98f206..e63848b 100644 --- a/tests/snippets/106_timers_list.py +++ b/tests/snippets/106_timers_list.py @@ -1,4 +1,4 @@ -# mcrf: objects=[Caption,Color,Frame,Scene,Timer] verified=0.2.8-dev@a7ba486 status=ok +# mcrf: objects=[Caption,Color,Frame,Scene,Timer] verified=0.2.8-dev status=ok # Timers List - View all active timers import mcrfpy diff --git a/tests/snippets/107_animations_list.py b/tests/snippets/107_animations_list.py index aa4756e..34bba86 100644 --- a/tests/snippets/107_animations_list.py +++ b/tests/snippets/107_animations_list.py @@ -1,4 +1,4 @@ -# mcrf: objects=[Caption,Color,Easing,Frame,Scene,Timer] verified=0.2.8-dev@a7ba486 status=ok +# mcrf: objects=[Caption,Color,Easing,Frame,Scene,Timer] verified=0.2.8-dev status=ok # Animations List - View all active animations import mcrfpy diff --git a/tests/snippets/108_cache_subtree.py b/tests/snippets/108_cache_subtree.py index fa025a0..3ab3280 100644 --- a/tests/snippets/108_cache_subtree.py +++ b/tests/snippets/108_cache_subtree.py @@ -1,4 +1,4 @@ -# mcrf: objects=[Caption,Color,Frame,Scene] verified=0.2.8-dev@a7ba486 status=ok +# mcrf: objects=[Caption,Color,Frame,Scene] verified=0.2.8-dev status=ok # Cache Subtree - Performance optimization import mcrfpy diff --git a/tests/snippets/109_realign.py b/tests/snippets/109_realign.py index 3399157..328e8ac 100644 --- a/tests/snippets/109_realign.py +++ b/tests/snippets/109_realign.py @@ -1,4 +1,4 @@ -# mcrf: objects=[Alignment,Caption,Color,Frame,InputState,Key,Scene] verified=0.2.8-dev@a7ba486 status=ok +# mcrf: objects=[Alignment,Caption,Color,Frame,InputState,Key,Scene] verified=0.2.8-dev status=ok # Realign - Reapply alignment after changes import mcrfpy diff --git a/tests/snippets/110_version_info.py b/tests/snippets/110_version_info.py index 7e51848..a663abe 100644 --- a/tests/snippets/110_version_info.py +++ b/tests/snippets/110_version_info.py @@ -1,4 +1,4 @@ -# mcrf: objects=[Caption,Color,Frame,Scene,Sprite] verified=0.2.8-dev@a7ba486 status=ok +# mcrf: objects=[Caption,Color,Frame,Scene,Sprite] verified=0.2.8-dev status=ok # Version Info - Engine version and info import mcrfpy diff --git a/tests/snippets/111_alignment_margins.py b/tests/snippets/111_alignment_margins.py index acfb336..ab791d1 100644 --- a/tests/snippets/111_alignment_margins.py +++ b/tests/snippets/111_alignment_margins.py @@ -1,4 +1,4 @@ -# mcrf: objects=[Alignment,Caption,Color,Frame,Scene] verified=0.2.8-dev@a7ba486 status=ok +# mcrf: objects=[Alignment,Caption,Color,Frame,Scene] verified=0.2.8-dev status=ok # Alignment Margins - Spacing from edges import mcrfpy diff --git a/tests/snippets/112_animation_delta.py b/tests/snippets/112_animation_delta.py index a6aebd9..80cb7c7 100644 --- a/tests/snippets/112_animation_delta.py +++ b/tests/snippets/112_animation_delta.py @@ -1,4 +1,4 @@ -# mcrf: objects=[Caption,Color,Easing,Frame,InputState,Key,Scene] verified=0.2.8-dev@a7ba486 status=ok +# mcrf: objects=[Caption,Color,Easing,Frame,InputState,Key,Scene] verified=0.2.8-dev status=ok # Animation Delta - Relative animation import mcrfpy diff --git a/tests/snippets/113_easing_cubic.py b/tests/snippets/113_easing_cubic.py index 2760134..21936d4 100644 --- a/tests/snippets/113_easing_cubic.py +++ b/tests/snippets/113_easing_cubic.py @@ -1,4 +1,4 @@ -# mcrf: objects=[Caption,Color,Easing,Frame,Scene,Timer] verified=0.2.8-dev@a7ba486 status=ok +# mcrf: objects=[Caption,Color,Easing,Frame,Scene,Timer] verified=0.2.8-dev status=ok # Easing Cubic - Cubic easing functions import mcrfpy diff --git a/tests/snippets/114_easing_sine.py b/tests/snippets/114_easing_sine.py index 2709744..5382394 100644 --- a/tests/snippets/114_easing_sine.py +++ b/tests/snippets/114_easing_sine.py @@ -1,4 +1,4 @@ -# mcrf: objects=[Caption,Color,Easing,Frame,Scene,Timer] verified=0.2.8-dev@a7ba486 status=ok +# mcrf: objects=[Caption,Color,Easing,Frame,Scene,Timer] verified=0.2.8-dev status=ok # Easing Sine - Sinusoidal easing import mcrfpy diff --git a/tests/snippets/115_easing_expo.py b/tests/snippets/115_easing_expo.py index d6ee431..13bf35f 100644 --- a/tests/snippets/115_easing_expo.py +++ b/tests/snippets/115_easing_expo.py @@ -1,4 +1,4 @@ -# mcrf: objects=[Caption,Color,Easing,Frame,Scene,Timer] verified=0.2.8-dev@a7ba486 status=ok +# mcrf: objects=[Caption,Color,Easing,Frame,Scene,Timer] verified=0.2.8-dev status=ok # Easing Exponential - Dramatic acceleration import mcrfpy diff --git a/tests/snippets/116_easing_circ.py b/tests/snippets/116_easing_circ.py index a4cc7a5..230d38f 100644 --- a/tests/snippets/116_easing_circ.py +++ b/tests/snippets/116_easing_circ.py @@ -1,4 +1,4 @@ -# mcrf: objects=[Caption,Color,Easing,Frame,Scene,Timer] verified=0.2.8-dev@a7ba486 status=ok +# mcrf: objects=[Caption,Color,Easing,Frame,Scene,Timer] verified=0.2.8-dev status=ok # Easing Circular - Circular motion curve import mcrfpy diff --git a/tests/snippets/117_easing_quart.py b/tests/snippets/117_easing_quart.py index f2135c4..62a9b53 100644 --- a/tests/snippets/117_easing_quart.py +++ b/tests/snippets/117_easing_quart.py @@ -1,4 +1,4 @@ -# mcrf: objects=[Caption,Color,Easing,Frame,Scene,Timer] verified=0.2.8-dev@a7ba486 status=ok +# mcrf: objects=[Caption,Color,Easing,Frame,Scene,Timer] verified=0.2.8-dev status=ok # Easing Quartic - Strong power easing import mcrfpy diff --git a/tests/snippets/118_ui_modal.py b/tests/snippets/118_ui_modal.py index 2b4d0da..cc0f26a 100644 --- a/tests/snippets/118_ui_modal.py +++ b/tests/snippets/118_ui_modal.py @@ -1,4 +1,4 @@ -# mcrf: objects=[Caption,Color,Easing,Frame,InputState,Key,Scene] verified=0.2.8-dev@a7ba486 status=ok +# mcrf: objects=[Caption,Color,Easing,Frame,InputState,Key,Scene] verified=0.2.8-dev status=ok # UI Modal Dialog - Popup confirmation import mcrfpy diff --git a/tests/snippets/119_game_collision.py b/tests/snippets/119_game_collision.py index 38570d1..32ba813 100644 --- a/tests/snippets/119_game_collision.py +++ b/tests/snippets/119_game_collision.py @@ -1,4 +1,4 @@ -# mcrf: objects=[Caption,Color,Entity,Grid,InputState,Key,Scene] verified=0.2.8-dev@a7ba486 status=ok +# mcrf: objects=[Caption,Color,Entity,Grid,InputState,Key,Scene] verified=0.2.8-dev status=ok # Game Collision - Simple collision detection import mcrfpy diff --git a/tests/snippets/120_animation_complete.py b/tests/snippets/120_animation_complete.py index c2a8f05..65a1283 100644 --- a/tests/snippets/120_animation_complete.py +++ b/tests/snippets/120_animation_complete.py @@ -1,4 +1,4 @@ -# mcrf: objects=[Caption,Color,Easing,Frame,InputState,Key,Scene,Timer] verified=0.2.8-dev@a7ba486 status=ok +# mcrf: objects=[Caption,Color,Easing,Frame,InputState,Key,Scene,Timer] verified=0.2.8-dev status=ok # Animation Complete Check - Monitor animation state import mcrfpy diff --git a/tests/snippets/121_entity_at.py b/tests/snippets/121_entity_at.py index f462354..061eac9 100644 --- a/tests/snippets/121_entity_at.py +++ b/tests/snippets/121_entity_at.py @@ -1,4 +1,4 @@ -# mcrf: objects=[Caption,Color,Entity,Grid,InputState,Scene] verified=0.2.8-dev@a7ba486 status=ok +# mcrf: objects=[Caption,Color,Entity,Grid,InputState,Scene] verified=0.2.8-dev status=ok # Entity At - Query entity's view of grid import mcrfpy diff --git a/tests/snippets/122_default_fov.py b/tests/snippets/122_default_fov.py index 7507bc7..0d78538 100644 --- a/tests/snippets/122_default_fov.py +++ b/tests/snippets/122_default_fov.py @@ -1,4 +1,4 @@ -# mcrf: objects=[Caption,Color,Entity,FOV,Frame,Grid,Scene] verified=0.2.8-dev@a7ba486 status=ok +# mcrf: objects=[Caption,Color,Entity,FOV,Frame,Grid,Scene] verified=0.2.8-dev status=ok # Default FOV - Set default algorithm import mcrfpy diff --git a/tests/snippets/123_animation_stop.py b/tests/snippets/123_animation_stop.py index c4df6ec..3b770e0 100644 --- a/tests/snippets/123_animation_stop.py +++ b/tests/snippets/123_animation_stop.py @@ -1,4 +1,4 @@ -# mcrf: objects=[Caption,Color,Easing,Frame,InputState,Key,Scene] verified=0.2.8-dev@a7ba486 status=ok +# mcrf: objects=[Caption,Color,Easing,Frame,InputState,Key,Scene] verified=0.2.8-dev status=ok # Animation Stop/Complete - Control running animations import mcrfpy diff --git a/tests/snippets/124_sprite_rotation.py b/tests/snippets/124_sprite_rotation.py index a2f6071..dc4e65d 100644 --- a/tests/snippets/124_sprite_rotation.py +++ b/tests/snippets/124_sprite_rotation.py @@ -1,4 +1,4 @@ -# mcrf: objects=[Caption,Color,Frame,Scene,Sprite] verified=0.2.8-dev@a7ba486 status=ok +# mcrf: objects=[Caption,Color,Frame,Scene,Sprite] verified=0.2.8-dev status=ok # Sprite Rotation - Rotate sprite images import mcrfpy diff --git a/tests/snippets/125_vector_normalized.py b/tests/snippets/125_vector_normalized.py index 7b1871e..ef41537 100644 --- a/tests/snippets/125_vector_normalized.py +++ b/tests/snippets/125_vector_normalized.py @@ -1,4 +1,4 @@ -# mcrf: objects=[Caption,Color,Frame,Scene,Vector] verified=0.2.8-dev@a7ba486 status=ok +# mcrf: objects=[Caption,Color,Frame,Scene,Vector] verified=0.2.8-dev status=ok # Vector Normalized - Unit vectors import mcrfpy diff --git a/tests/snippets/126_timer_restart.py b/tests/snippets/126_timer_restart.py index 2b368ed..629362c 100644 --- a/tests/snippets/126_timer_restart.py +++ b/tests/snippets/126_timer_restart.py @@ -1,4 +1,4 @@ -# mcrf: objects=[Caption,Color,Frame,InputState,Key,Scene,Timer] verified=0.2.8-dev@a7ba486 status=ok +# mcrf: objects=[Caption,Color,Frame,InputState,Key,Scene,Timer] verified=0.2.8-dev status=ok # Timer Restart - Reset and start timer import mcrfpy diff --git a/tests/snippets/127_grid_size.py b/tests/snippets/127_grid_size.py index 0f20479..1196f65 100644 --- a/tests/snippets/127_grid_size.py +++ b/tests/snippets/127_grid_size.py @@ -1,4 +1,4 @@ -# mcrf: objects=[Caption,Color,Entity,Frame,Grid,Scene] verified=0.2.8-dev@a7ba486 status=ok +# mcrf: objects=[Caption,Color,Entity,Frame,Grid,Scene] verified=0.2.8-dev status=ok # Grid Display Size - Viewport dimensions import mcrfpy diff --git a/tests/snippets/128_color_animation_channels.py b/tests/snippets/128_color_animation_channels.py index 160ef63..f034ea8 100644 --- a/tests/snippets/128_color_animation_channels.py +++ b/tests/snippets/128_color_animation_channels.py @@ -1,4 +1,4 @@ -# mcrf: objects=[Caption,Color,Easing,Frame,Scene,Timer] verified=0.2.8-dev@a7ba486 status=ok +# mcrf: objects=[Caption,Color,Easing,Frame,Scene,Timer] verified=0.2.8-dev status=ok # Color Channel Animation - Animate individual RGBA import mcrfpy diff --git a/tests/snippets/129_scene_update.py b/tests/snippets/129_scene_update.py index b064b62..fe27872 100644 --- a/tests/snippets/129_scene_update.py +++ b/tests/snippets/129_scene_update.py @@ -1,4 +1,4 @@ -# mcrf: objects=[Caption,Color,Frame,Scene] verified=0.2.8-dev@a7ba486 status=ok +# mcrf: objects=[Caption,Color,Frame,Scene] verified=0.2.8-dev status=ok # Scene Update - Per-frame update method import mcrfpy import math diff --git a/tests/snippets/130_exit_game.py b/tests/snippets/130_exit_game.py index 81c348f..a258d7b 100644 --- a/tests/snippets/130_exit_game.py +++ b/tests/snippets/130_exit_game.py @@ -1,4 +1,4 @@ -# mcrf: objects=[Caption,Color,Frame,InputState,Key,Scene,Timer] verified=0.2.8-dev@a7ba486 status=ok +# mcrf: objects=[Caption,Color,Frame,InputState,Key,Scene,Timer] verified=0.2.8-dev status=ok # Exit Game - Clean shutdown import mcrfpy diff --git a/tools/stamp_snippets.py b/tools/stamp_snippets.py index a95e15a..5a15855 100644 --- a/tools/stamp_snippets.py +++ b/tools/stamp_snippets.py @@ -19,7 +19,8 @@ touched the file, and 130 of them said "ok" while nothing had ever executed them script makes the header a MEASUREMENT: * `status` comes from actually running the snippet through tests/snippets/_harness.py - * `verified` comes from the engine that ran it (version@commit, out of api/manifest.json) + * `verified` comes from the engine that ran it (the release version, out of + api/manifest.json) -- a stable "0.2.8-dev" during a cycle, "0.2.9" at a tag * `objects` is derived from the source, intersected with the objects the engine actually exports -- so a snippet that starts using a new type gets tagged for it without anyone remembering to. @@ -119,9 +120,14 @@ def derive_objects(source, known): return sorted(used & known) -def build_header(objects, version, commit, status): +def build_header(objects, version, status): + # `verified` is the release version, NOT version@commit. A commit hash would churn + # every one of these headers on every commit for zero signal git history doesn't + # already carry -- and `--check` ignores `verified` anyway. During a dev cycle the + # version is a stable "0.2.8-dev"; at a tagged release it becomes "0.2.9", which is + # the marker that freezes a version into the docs site's history. return (f"# mcrf: objects=[{','.join(objects)}] " - f"verified={version}@{commit} status={status}") + f"verified={version} status={status}") def restamp(source, header): @@ -176,7 +182,7 @@ def main(): f"!= actual {objects}") continue - updated = restamp(source, build_header(objects, version, commit, status)) + updated = restamp(source, build_header(objects, version, status)) if updated != source: with open(path, "w", encoding="utf-8") as f: f.write(updated) From 07442dd0f0b0c2ef6b3eda9aa4b6567e231cfe90 Mon Sep 17 00:00:00 2001 From: John McCardle Date: Wed, 15 Jul 2026 00:26:38 -0400 Subject: [PATCH 5/5] test(snippets): promote 152 hand-written docs examples into the gated suite Extends the docs-as-tests pipeline to the hand-written pages. A multi-agent workflow (tools/wf_docs_rewrite.js) fixed every cookbook/objects/tutorial/ features/templates page against the live 0.2.8-dev API and extracted every complete runnable example into tests/snippets/131-282, each validated through _harness.py and stamped by stamp_snippets.py. run_tests.py already discovers tests/snippets/, so these 152 are now permanently gated: a future API break that invalidates a documented example fails the suite. Suite is 282/282; `make check-snippets` clean. --- tests/snippets/131_combat_melee.py | 241 +++ tests/snippets/132_effects_color_pulse_1.py | 52 + tests/snippets/133_effects_color_pulse_2.py | 78 + tests/snippets/134_effects_color_pulse_3.py | 133 ++ tests/snippets/135_effects_color_pulse_4.py | 106 + tests/snippets/136_effects_color_pulse_5.py | 66 + tests/snippets/137_effects_damage_flash_1.py | 40 + tests/snippets/138_effects_damage_flash_2.py | 95 + tests/snippets/139_effects_damage_flash_3.py | 36 + tests/snippets/140_effects_floating_text.py | 30 + tests/snippets/141_effects_path_animation.py | 88 + .../142_effects_scene_transitions_1.py | 12 + .../143_effects_scene_transitions_2.py | 77 + .../144_effects_scene_transitions_3.py | 57 + .../145_effects_scene_transitions_4.py | 90 + .../146_effects_scene_transitions_5.py | 159 ++ tests/snippets/147_effects_screen_shake_1.py | 32 + tests/snippets/148_effects_screen_shake_2.py | 47 + tests/snippets/149_effects_screen_shake_3.py | 51 + .../150_grid_interaction_patterns_1.py | 35 + .../151_grid_interaction_patterns_2.py | 42 + .../152_grid_interaction_patterns_3.py | 47 + .../153_grid_interaction_patterns_4.py | 48 + .../154_grid_interaction_patterns_5.py | 65 + .../155_grid_interaction_patterns_6.py | 50 + .../156_grid_interaction_patterns_7.py | 60 + .../snippets/157_grid_cell_highlighting_1.py | 30 + .../snippets/158_grid_cell_highlighting_2.py | 69 + tests/snippets/159_grid_dijkstra_1.py | 61 + tests/snippets/160_grid_dijkstra_2.py | 43 + .../snippets/161_grid_dijkstra_heightmap_1.py | 21 + .../snippets/162_grid_dijkstra_heightmap_2.py | 54 + tests/snippets/163_grid_dungeon_generator.py | 206 ++ tests/snippets/164_grid_fog_of_war_1.py | 20 + tests/snippets/165_grid_fog_of_war_2.py | 87 + tests/snippets/166_grid_multi_layer_1.py | 30 + tests/snippets/167_grid_multi_layer_2.py | 118 ++ tests/snippets/168_headless_mode_1.py | 18 + tests/snippets/169_headless_mode_2.py | 20 + tests/snippets/170_procgen_cellular_caves.py | 184 ++ tests/snippets/171_procgen_wfc.py | 140 ++ tests/snippets/172_rendering_and_visuals_1.py | 25 + tests/snippets/173_rendering_and_visuals_2.py | 25 + tests/snippets/174_ui_widget_patterns_1.py | 18 + tests/snippets/175_ui_widget_patterns_2.py | 55 + tests/snippets/176_ui_widget_patterns_3.py | 61 + tests/snippets/177_ui_widget_patterns_4.py | 86 + tests/snippets/178_ui_widget_patterns_5.py | 75 + tests/snippets/179_ui_widget_patterns_6.py | 75 + tests/snippets/180_ui_widget_patterns_7.py | 66 + tests/snippets/181_ui_alignment_1.py | 18 + tests/snippets/182_ui_alignment_2.py | 24 + tests/snippets/183_ui_alignment_3.py | 20 + tests/snippets/184_ui_alignment_4.py | 20 + tests/snippets/185_ui_alignment_5.py | 41 + tests/snippets/186_ui_alignment_6.py | 36 + tests/snippets/187_ui_alignment_7.py | 35 + tests/snippets/188_ui_health_bar_1.py | 72 + tests/snippets/189_ui_health_bar_2.py | 112 ++ tests/snippets/190_ui_health_bar_3.py | 100 + tests/snippets/191_ui_health_bar_4.py | 120 ++ tests/snippets/192_ui_menu_1.py | 153 ++ tests/snippets/193_ui_menu_2.py | 282 +++ tests/snippets/194_ui_menu_3.py | 162 ++ tests/snippets/195_ui_message_log_1.py | 109 + tests/snippets/196_ui_message_log_2.py | 140 ++ tests/snippets/197_ui_message_log_3.py | 145 ++ tests/snippets/198_ui_modal_dialog_1.py | 180 ++ tests/snippets/199_ui_modal_dialog_2.py | 332 +++ tests/snippets/200_ui_tooltip_1.py | 123 ++ tests/snippets/201_ui_tooltip_2.py | 169 ++ tests/snippets/202_ui_tooltip_3.py | 63 + tests/snippets/203_ui_tooltip_4.py | 134 ++ tests/snippets/204_fov.py | 101 + tests/snippets/205_grid_system_1.py | 17 + tests/snippets/206_grid_system_2.py | 94 + tests/snippets/207_pathfinding.py | 112 ++ tests/snippets/208_scenes_1.py | 20 + tests/snippets/209_scenes_2.py | 109 + tests/snippets/210_AStarPath.py | 34 + tests/snippets/211_Alignment.py | 20 + tests/snippets/212_Animation_1.py | 22 + tests/snippets/213_Animation_2.py | 16 + tests/snippets/214_Animation_3.py | 23 + tests/snippets/215_Animation_4.py | 19 + tests/snippets/216_Animation_5.py | 20 + tests/snippets/217_Animation_6.py | 15 + tests/snippets/218_Animation_7.py | 17 + tests/snippets/219_Animation_8.py | 16 + tests/snippets/220_Animation_9.py | 17 + tests/snippets/221_Animation_10.py | 11 + tests/snippets/222_Arc_1.py | 26 + tests/snippets/223_Arc_2.py | 20 + tests/snippets/224_BSP_1.py | 42 + tests/snippets/225_BSP_2.py | 31 + tests/snippets/226_BSP_3.py | 27 + tests/snippets/227_Caption.py | 24 + tests/snippets/228_Circle_1.py | 22 + tests/snippets/229_Circle_2.py | 29 + tests/snippets/230_ColorLayer_1.py | 41 + tests/snippets/231_ColorLayer_2.py | 41 + tests/snippets/232_Color.py | 30 + tests/snippets/233_DijkstraMap.py | 61 + tests/snippets/234_Drawable.py | 29 + tests/snippets/235_Easing.py | 18 + tests/snippets/236_Entity.py | 42 + tests/snippets/237_FOV_1.py | 26 + tests/snippets/238_FOV_2.py | 20 + tests/snippets/239_Font_1.py | 21 + tests/snippets/240_Font_2.py | 20 + tests/snippets/241_Frame.py | 24 + tests/snippets/242_Grid.py | 42 + tests/snippets/243_HeightMap.py | 33 + tests/snippets/244_InputState.py | 13 + tests/snippets/245_Key.py | 19 + tests/snippets/246_Keyboard.py | 22 + tests/snippets/247_Line_1.py | 22 + tests/snippets/248_Line_2.py | 30 + tests/snippets/249_MouseButton_1.py | 31 + tests/snippets/250_MouseButton_2.py | 45 + tests/snippets/251_Mouse_1.py | 22 + tests/snippets/252_Mouse_2.py | 19 + tests/snippets/253_Music.py | 21 + tests/snippets/254_NoiseSource_1.py | 23 + tests/snippets/255_NoiseSource_2.py | 32 + tests/snippets/256_Scene_1.py | 44 + tests/snippets/257_Scene_2.py | 18 + tests/snippets/258_Sprite.py | 25 + tests/snippets/259_Texture_1.py | 16 + tests/snippets/260_Texture_2.py | 30 + tests/snippets/261_TileLayer.py | 33 + tests/snippets/262_Timer.py | 35 + tests/snippets/263_Transition_1.py | 23 + tests/snippets/264_Transition_2.py | 38 + tests/snippets/265_Traversal.py | 48 + tests/snippets/266_Vector_1.py | 31 + tests/snippets/267_Vector_2.py | 33 + tests/snippets/268_Window.py | 21 + tests/snippets/269_part_00_setup.py | 26 + tests/snippets/270_part_01_grid_movement.py | 116 ++ .../snippets/271_part_02_tiles_collision_1.py | 202 ++ .../snippets/272_part_02_tiles_collision_2.py | 106 + .../273_part_03_dungeon_generation.py | 352 ++++ tests/snippets/274_part_04_fov.py | 367 ++++ tests/snippets/275_part_05_enemies.py | 673 +++++++ tests/snippets/276_part_06_combat.py | 930 +++++++++ tests/snippets/277_part_07_ui.py | 1028 ++++++++++ tests/snippets/278_part_08_items.py | 1274 ++++++++++++ tests/snippets/279_part_09_ranged.py | 1395 +++++++++++++ tests/snippets/280_part_10_save_load.py | 1561 ++++++++++++++ tests/snippets/281_part_11_levels.py | 1732 ++++++++++++++++ tests/snippets/282_part_13_equipment.py | 1791 +++++++++++++++++ tools/wf_docs_rewrite.js | 256 +++ 153 files changed, 19869 insertions(+) create mode 100644 tests/snippets/131_combat_melee.py create mode 100644 tests/snippets/132_effects_color_pulse_1.py create mode 100644 tests/snippets/133_effects_color_pulse_2.py create mode 100644 tests/snippets/134_effects_color_pulse_3.py create mode 100644 tests/snippets/135_effects_color_pulse_4.py create mode 100644 tests/snippets/136_effects_color_pulse_5.py create mode 100644 tests/snippets/137_effects_damage_flash_1.py create mode 100644 tests/snippets/138_effects_damage_flash_2.py create mode 100644 tests/snippets/139_effects_damage_flash_3.py create mode 100644 tests/snippets/140_effects_floating_text.py create mode 100644 tests/snippets/141_effects_path_animation.py create mode 100644 tests/snippets/142_effects_scene_transitions_1.py create mode 100644 tests/snippets/143_effects_scene_transitions_2.py create mode 100644 tests/snippets/144_effects_scene_transitions_3.py create mode 100644 tests/snippets/145_effects_scene_transitions_4.py create mode 100644 tests/snippets/146_effects_scene_transitions_5.py create mode 100644 tests/snippets/147_effects_screen_shake_1.py create mode 100644 tests/snippets/148_effects_screen_shake_2.py create mode 100644 tests/snippets/149_effects_screen_shake_3.py create mode 100644 tests/snippets/150_grid_interaction_patterns_1.py create mode 100644 tests/snippets/151_grid_interaction_patterns_2.py create mode 100644 tests/snippets/152_grid_interaction_patterns_3.py create mode 100644 tests/snippets/153_grid_interaction_patterns_4.py create mode 100644 tests/snippets/154_grid_interaction_patterns_5.py create mode 100644 tests/snippets/155_grid_interaction_patterns_6.py create mode 100644 tests/snippets/156_grid_interaction_patterns_7.py create mode 100644 tests/snippets/157_grid_cell_highlighting_1.py create mode 100644 tests/snippets/158_grid_cell_highlighting_2.py create mode 100644 tests/snippets/159_grid_dijkstra_1.py create mode 100644 tests/snippets/160_grid_dijkstra_2.py create mode 100644 tests/snippets/161_grid_dijkstra_heightmap_1.py create mode 100644 tests/snippets/162_grid_dijkstra_heightmap_2.py create mode 100644 tests/snippets/163_grid_dungeon_generator.py create mode 100644 tests/snippets/164_grid_fog_of_war_1.py create mode 100644 tests/snippets/165_grid_fog_of_war_2.py create mode 100644 tests/snippets/166_grid_multi_layer_1.py create mode 100644 tests/snippets/167_grid_multi_layer_2.py create mode 100644 tests/snippets/168_headless_mode_1.py create mode 100644 tests/snippets/169_headless_mode_2.py create mode 100644 tests/snippets/170_procgen_cellular_caves.py create mode 100644 tests/snippets/171_procgen_wfc.py create mode 100644 tests/snippets/172_rendering_and_visuals_1.py create mode 100644 tests/snippets/173_rendering_and_visuals_2.py create mode 100644 tests/snippets/174_ui_widget_patterns_1.py create mode 100644 tests/snippets/175_ui_widget_patterns_2.py create mode 100644 tests/snippets/176_ui_widget_patterns_3.py create mode 100644 tests/snippets/177_ui_widget_patterns_4.py create mode 100644 tests/snippets/178_ui_widget_patterns_5.py create mode 100644 tests/snippets/179_ui_widget_patterns_6.py create mode 100644 tests/snippets/180_ui_widget_patterns_7.py create mode 100644 tests/snippets/181_ui_alignment_1.py create mode 100644 tests/snippets/182_ui_alignment_2.py create mode 100644 tests/snippets/183_ui_alignment_3.py create mode 100644 tests/snippets/184_ui_alignment_4.py create mode 100644 tests/snippets/185_ui_alignment_5.py create mode 100644 tests/snippets/186_ui_alignment_6.py create mode 100644 tests/snippets/187_ui_alignment_7.py create mode 100644 tests/snippets/188_ui_health_bar_1.py create mode 100644 tests/snippets/189_ui_health_bar_2.py create mode 100644 tests/snippets/190_ui_health_bar_3.py create mode 100644 tests/snippets/191_ui_health_bar_4.py create mode 100644 tests/snippets/192_ui_menu_1.py create mode 100644 tests/snippets/193_ui_menu_2.py create mode 100644 tests/snippets/194_ui_menu_3.py create mode 100644 tests/snippets/195_ui_message_log_1.py create mode 100644 tests/snippets/196_ui_message_log_2.py create mode 100644 tests/snippets/197_ui_message_log_3.py create mode 100644 tests/snippets/198_ui_modal_dialog_1.py create mode 100644 tests/snippets/199_ui_modal_dialog_2.py create mode 100644 tests/snippets/200_ui_tooltip_1.py create mode 100644 tests/snippets/201_ui_tooltip_2.py create mode 100644 tests/snippets/202_ui_tooltip_3.py create mode 100644 tests/snippets/203_ui_tooltip_4.py create mode 100644 tests/snippets/204_fov.py create mode 100644 tests/snippets/205_grid_system_1.py create mode 100644 tests/snippets/206_grid_system_2.py create mode 100644 tests/snippets/207_pathfinding.py create mode 100644 tests/snippets/208_scenes_1.py create mode 100644 tests/snippets/209_scenes_2.py create mode 100644 tests/snippets/210_AStarPath.py create mode 100644 tests/snippets/211_Alignment.py create mode 100644 tests/snippets/212_Animation_1.py create mode 100644 tests/snippets/213_Animation_2.py create mode 100644 tests/snippets/214_Animation_3.py create mode 100644 tests/snippets/215_Animation_4.py create mode 100644 tests/snippets/216_Animation_5.py create mode 100644 tests/snippets/217_Animation_6.py create mode 100644 tests/snippets/218_Animation_7.py create mode 100644 tests/snippets/219_Animation_8.py create mode 100644 tests/snippets/220_Animation_9.py create mode 100644 tests/snippets/221_Animation_10.py create mode 100644 tests/snippets/222_Arc_1.py create mode 100644 tests/snippets/223_Arc_2.py create mode 100644 tests/snippets/224_BSP_1.py create mode 100644 tests/snippets/225_BSP_2.py create mode 100644 tests/snippets/226_BSP_3.py create mode 100644 tests/snippets/227_Caption.py create mode 100644 tests/snippets/228_Circle_1.py create mode 100644 tests/snippets/229_Circle_2.py create mode 100644 tests/snippets/230_ColorLayer_1.py create mode 100644 tests/snippets/231_ColorLayer_2.py create mode 100644 tests/snippets/232_Color.py create mode 100644 tests/snippets/233_DijkstraMap.py create mode 100644 tests/snippets/234_Drawable.py create mode 100644 tests/snippets/235_Easing.py create mode 100644 tests/snippets/236_Entity.py create mode 100644 tests/snippets/237_FOV_1.py create mode 100644 tests/snippets/238_FOV_2.py create mode 100644 tests/snippets/239_Font_1.py create mode 100644 tests/snippets/240_Font_2.py create mode 100644 tests/snippets/241_Frame.py create mode 100644 tests/snippets/242_Grid.py create mode 100644 tests/snippets/243_HeightMap.py create mode 100644 tests/snippets/244_InputState.py create mode 100644 tests/snippets/245_Key.py create mode 100644 tests/snippets/246_Keyboard.py create mode 100644 tests/snippets/247_Line_1.py create mode 100644 tests/snippets/248_Line_2.py create mode 100644 tests/snippets/249_MouseButton_1.py create mode 100644 tests/snippets/250_MouseButton_2.py create mode 100644 tests/snippets/251_Mouse_1.py create mode 100644 tests/snippets/252_Mouse_2.py create mode 100644 tests/snippets/253_Music.py create mode 100644 tests/snippets/254_NoiseSource_1.py create mode 100644 tests/snippets/255_NoiseSource_2.py create mode 100644 tests/snippets/256_Scene_1.py create mode 100644 tests/snippets/257_Scene_2.py create mode 100644 tests/snippets/258_Sprite.py create mode 100644 tests/snippets/259_Texture_1.py create mode 100644 tests/snippets/260_Texture_2.py create mode 100644 tests/snippets/261_TileLayer.py create mode 100644 tests/snippets/262_Timer.py create mode 100644 tests/snippets/263_Transition_1.py create mode 100644 tests/snippets/264_Transition_2.py create mode 100644 tests/snippets/265_Traversal.py create mode 100644 tests/snippets/266_Vector_1.py create mode 100644 tests/snippets/267_Vector_2.py create mode 100644 tests/snippets/268_Window.py create mode 100644 tests/snippets/269_part_00_setup.py create mode 100644 tests/snippets/270_part_01_grid_movement.py create mode 100644 tests/snippets/271_part_02_tiles_collision_1.py create mode 100644 tests/snippets/272_part_02_tiles_collision_2.py create mode 100644 tests/snippets/273_part_03_dungeon_generation.py create mode 100644 tests/snippets/274_part_04_fov.py create mode 100644 tests/snippets/275_part_05_enemies.py create mode 100644 tests/snippets/276_part_06_combat.py create mode 100644 tests/snippets/277_part_07_ui.py create mode 100644 tests/snippets/278_part_08_items.py create mode 100644 tests/snippets/279_part_09_ranged.py create mode 100644 tests/snippets/280_part_10_save_load.py create mode 100644 tests/snippets/281_part_11_levels.py create mode 100644 tests/snippets/282_part_13_equipment.py create mode 100644 tools/wf_docs_rewrite.js diff --git a/tests/snippets/131_combat_melee.py b/tests/snippets/131_combat_melee.py new file mode 100644 index 0000000..45e5aa2 --- /dev/null +++ b/tests/snippets/131_combat_melee.py @@ -0,0 +1,241 @@ +# mcrf: objects=[Caption,Color,Easing,Entity,Frame,Grid,Scene,Timer] verified=0.2.8-dev status=ok +# Melee combat cookbook recipe - Fighter stats, attack resolution, CombatEntity, +# health bars, and floating damage numbers, all combined into one runnable demo. +import random +from dataclasses import dataclass + +import mcrfpy + + +@dataclass +class Fighter: + """Combat statistics for an entity.""" + hp: int + max_hp: int + attack: int + defense: int + + def is_alive(self) -> bool: + return self.hp > 0 + + def heal(self, amount: int): + """Heal up to max HP.""" + self.hp = min(self.max_hp, self.hp + amount) + + def take_damage(self, amount: int) -> int: + """Take damage and return actual damage dealt.""" + actual = min(self.hp, amount) + self.hp -= actual + return actual + + +def calculate_damage(attacker: Fighter, defender: Fighter) -> dict: + """ + Calculate damage with variance and critical hits. + + Formula: (ATK * variance) - DEF, minimum 1 + Critical: 10% chance for 2x damage, ignores defense + """ + variance = random.uniform(0.8, 1.2) + base_damage = int(attacker.attack * variance) + + crit_chance = 0.10 + is_critical = random.random() < crit_chance + + if is_critical: + final_damage = base_damage * 2 + else: + final_damage = max(1, base_damage - defender.defense) + + return { + "damage": final_damage, + "is_critical": is_critical, + "base_roll": base_damage, + } + + +def attack_with_variance(attacker: Fighter, defender: Fighter) -> dict: + """Perform attack with variance and crits.""" + result = calculate_damage(attacker, defender) + actual = defender.take_damage(result["damage"]) + + return { + "damage": actual, + "is_critical": result["is_critical"], + "killed": not defender.is_alive(), + } + + +class CombatEntity: + """Game entity with combat capabilities.""" + + def __init__(self, grid, x, y, texture, sprite_index, + hp=10, attack=5, defense=2, name="Entity"): + # Create visual entity + self.entity = mcrfpy.Entity((x, y), texture, sprite_index) + grid.entities.append(self.entity) + self.grid = grid + + # Combat stats + self.fighter = Fighter(hp=hp, max_hp=hp, attack=attack, defense=defense) + self.name = name + + # State + self.alive = True + + @property + def x(self): + return self.entity.grid_x + + @property + def y(self): + return self.entity.grid_y + + @property + def hp(self): + return self.fighter.hp + + @hp.setter + def hp(self, value): + self.fighter.hp = value + if self.fighter.hp <= 0: + self.die() + + def attack_target(self, target: 'CombatEntity') -> dict: + """Attack another combat entity.""" + result = attack_with_variance(self.fighter, target.fighter) + + if result["is_critical"]: + print(f"{self.name} CRITICALLY hits {target.name} for {result['damage']}!") + else: + print(f"{self.name} hits {target.name} for {result['damage']}.") + + if result["killed"]: + target.die() + + return result + + def die(self): + """Handle death.""" + if not self.alive: + return + + self.alive = False + print(f"{self.name} has died!") + + # Remove from grid (Entity.die() detaches it from its grid) + try: + self.entity.die() + except Exception: + pass + + def heal(self, amount: int): + """Restore HP.""" + old_hp = self.fighter.hp + self.fighter.heal(amount) + healed = self.fighter.hp - old_hp + print(f"{self.name} heals for {healed} HP.") + + +class HealthBar: + """Visual health bar for an entity.""" + + def __init__(self, scene, combat_entity, offset_y=-10, width=32, height=4): + self.scene = scene + self.entity = combat_entity + self.offset_x = 0 + self.offset_y = offset_y + self.width = width + self.height = height + + # Background (red/empty) + self.bg = mcrfpy.Frame(pos=(0, 0), size=(width, height)) + self.bg.fill_color = mcrfpy.Color(100, 0, 0) + scene.children.append(self.bg) + + # Foreground (green/full) + self.fg = mcrfpy.Frame(pos=(0, 0), size=(width, height)) + self.fg.fill_color = mcrfpy.Color(0, 200, 0) + scene.children.append(self.fg) + + self.update() + + def update(self): + """Update health bar position and fill.""" + screen_x = self.entity.x * 16 + self.offset_x + screen_y = self.entity.y * 16 + self.offset_y + + self.bg.x = screen_x + self.bg.y = screen_y + + self.fg.x = screen_x + self.fg.y = screen_y + + hp_percent = self.entity.fighter.hp / self.entity.fighter.max_hp + self.fg.w = self.width * hp_percent + + if hp_percent > 0.6: + self.fg.fill_color = mcrfpy.Color(0, 200, 0) # Green + elif hp_percent > 0.3: + self.fg.fill_color = mcrfpy.Color(200, 200, 0) # Yellow + else: + self.fg.fill_color = mcrfpy.Color(200, 0, 0) # Red + + def remove(self): + """Remove health bar from UI.""" + for elem in (self.bg, self.fg): + try: + self.scene.children.remove(elem) + except ValueError: + pass + + +def show_damage_number(scene, x, y, damage, is_critical=False): + """Display floating damage number.""" + text = f"{damage}" if not is_critical else f"CRIT {damage}!" + color = mcrfpy.Color(255, 255, 0) if is_critical else mcrfpy.Color(255, 100, 100) + + caption = mcrfpy.Caption(text=text, x=x, y=y) + caption.fill_color = color + scene.children.append(caption) + + # Animate upward and fade out + caption.animate("y", float(y - 30), 0.8, mcrfpy.Easing.EASE_OUT) + + # Remove after animation + def cleanup(timer, runtime): + try: + scene.children.remove(caption) + except ValueError: + pass + + mcrfpy.Timer(f"dmg_{id(caption)}", cleanup, 800) + + +scene = mcrfpy.Scene("combat_demo") +mcrfpy.current_scene = scene + +grid = mcrfpy.Grid( + grid_size=(16, 12), + texture=mcrfpy.default_texture, + pos=(0, 0), + size=(1024, 768), + zoom=4.0, +) +scene.children.append(grid) + +for gy in range(12): + for gx in range(16): + grid.at(gx, gy).tilesprite = 48 + +hero = CombatEntity(grid, 3, 6, mcrfpy.default_texture, 84, + hp=20, attack=6, defense=2, name="Hero") +goblin = CombatEntity(grid, 5, 6, mcrfpy.default_texture, 112, + hp=12, attack=4, defense=1, name="Goblin") + +hero_bar = HealthBar(scene, hero) +goblin_bar = HealthBar(scene, goblin) + +result = hero.attack_target(goblin) +goblin_bar.update() +show_damage_number(scene, goblin.x * 16, goblin.y * 16, result["damage"], result["is_critical"]) diff --git a/tests/snippets/132_effects_color_pulse_1.py b/tests/snippets/132_effects_color_pulse_1.py new file mode 100644 index 0000000..b46484a --- /dev/null +++ b/tests/snippets/132_effects_color_pulse_1.py @@ -0,0 +1,52 @@ +# mcrf: objects=[Color,ColorLayer,Grid,Scene,Timer] verified=0.2.8-dev status=ok +import mcrfpy +import math + +def get_or_create_layer(grid, name="pulse", z_index=1): + """Find grid's named ColorLayer, or create and attach one.""" + existing = grid.layer(name) + if existing is not None: + return existing + layer = mcrfpy.ColorLayer(z_index=z_index, name=name) + grid.add_layer(layer) + return layer + +def pulse_cell(grid, x, y, color, duration=1.0): + """ + Pulse a cell's color overlay from transparent to visible and back once. + + Args: + grid: Grid to overlay + x, y: Cell coordinates + color: RGB tuple (r, g, b) + duration: Full cycle duration in seconds + """ + layer = get_or_create_layer(grid) + r, g, b = color + half_duration = duration / 2.0 + state = {"t0": None} + + def tick(timer, runtime): + if state["t0"] is None: + state["t0"] = runtime + t = (runtime - state["t0"]) / 1000.0 + if t >= duration: + layer.set((x, y), mcrfpy.Color(r, g, b, 0)) + timer.stop() + return + # Triangle wave: 0 -> 1 over the first half, 1 -> 0 over the second + phase = t / half_duration + alpha_frac = phase if phase <= 1.0 else 2.0 - phase + layer.set((x, y), mcrfpy.Color(r, g, b, int(200 * alpha_frac))) + + mcrfpy.Timer(f"pulse_{x}_{y}", tick, 33) + + +# Usage +scene = mcrfpy.Scene("demo") +mcrfpy.current_scene = scene + +grid = mcrfpy.Grid(grid_size=(16, 12), pos=(0, 0), size=(512, 384)) +scene.children.append(grid) + +pulse_cell(grid, 5, 5, (255, 200, 0), duration=1.5) # Golden pulse diff --git a/tests/snippets/133_effects_color_pulse_2.py b/tests/snippets/133_effects_color_pulse_2.py new file mode 100644 index 0000000..cb8bb03 --- /dev/null +++ b/tests/snippets/133_effects_color_pulse_2.py @@ -0,0 +1,78 @@ +# mcrf: objects=[Color,ColorLayer,Grid,Scene,Timer] verified=0.2.8-dev status=ok +import mcrfpy +import math + +class PulsingCell: + """A cell that continuously pulses until stopped.""" + + def __init__(self, grid, x, y, color, period=1.0, max_alpha=180): + """ + Args: + grid: Grid with color layer + x, y: Cell position + color: RGB tuple + period: Time for one complete pulse cycle + max_alpha: Maximum alpha value (0-255) + """ + self.grid = grid + self.x = x + self.y = y + self.color = color + self.period = period + self.max_alpha = max_alpha + self.is_pulsing = False + self.layer = self._get_or_create_layer() + self.timer = None + + def _get_or_create_layer(self): + """Ensure a named color layer exists on the grid.""" + existing = self.grid.layer("pulse") + if existing is not None: + return existing + layer = mcrfpy.ColorLayer(z_index=1, name="pulse") + self.grid.add_layer(layer) + return layer + + def _tick(self, timer, runtime): + if not self.is_pulsing: + return + r, g, b = self.color + # Sine wave oscillating between 0 and max_alpha + t = runtime / 1000.0 + wave = (math.sin(2 * math.pi * t / self.period) + 1.0) / 2.0 + alpha = int(self.max_alpha * wave) + self.layer.set((self.x, self.y), mcrfpy.Color(r, g, b, alpha)) + + def start(self): + """Start continuous pulsing.""" + if self.is_pulsing: + return + self.is_pulsing = True + self.timer = mcrfpy.Timer(f"pulse_{id(self)}", self._tick, 33) + + def stop(self): + """Stop pulsing and clear the cell.""" + self.is_pulsing = False + if self.timer is not None: + self.timer.stop() + self.timer = None + r, g, b = self.color + self.layer.set((self.x, self.y), mcrfpy.Color(r, g, b, 0)) + + def set_color(self, color): + """Change pulse color for subsequent ticks.""" + self.color = color + + +# Usage +scene = mcrfpy.Scene("demo") +mcrfpy.current_scene = scene + +grid = mcrfpy.Grid(grid_size=(16, 12), pos=(0, 0), size=(512, 384)) +scene.children.append(grid) + +objective_pulse = PulsingCell(grid, 10, 10, (0, 255, 100), period=1.5) +objective_pulse.start() + +# Later, when objective is reached: +objective_pulse.stop() diff --git a/tests/snippets/134_effects_color_pulse_3.py b/tests/snippets/134_effects_color_pulse_3.py new file mode 100644 index 0000000..21a5760 --- /dev/null +++ b/tests/snippets/134_effects_color_pulse_3.py @@ -0,0 +1,133 @@ +# mcrf: objects=[Color,ColorLayer,Grid,Scene,Timer] verified=0.2.8-dev status=ok +import mcrfpy +import math + +class PulsingCell: + """A cell that continuously pulses until stopped.""" + + def __init__(self, grid, x, y, color, period=1.0, max_alpha=180): + self.grid = grid + self.x = x + self.y = y + self.color = color + self.period = period + self.max_alpha = max_alpha + self.is_pulsing = False + self.layer = self._get_or_create_layer() + self.timer = None + + def _get_or_create_layer(self): + existing = self.grid.layer("pulse") + if existing is not None: + return existing + layer = mcrfpy.ColorLayer(z_index=1, name="pulse") + self.grid.add_layer(layer) + return layer + + def _tick(self, timer, runtime): + if not self.is_pulsing: + return + r, g, b = self.color + t = runtime / 1000.0 + wave = (math.sin(2 * math.pi * t / self.period) + 1.0) / 2.0 + alpha = int(self.max_alpha * wave) + self.layer.set((self.x, self.y), mcrfpy.Color(r, g, b, alpha)) + + def start(self): + if self.is_pulsing: + return + self.is_pulsing = True + self.timer = mcrfpy.Timer(f"pulse_{id(self)}", self._tick, 33) + + def stop(self): + self.is_pulsing = False + if self.timer is not None: + self.timer.stop() + self.timer = None + r, g, b = self.color + self.layer.set((self.x, self.y), mcrfpy.Color(r, g, b, 0)) + + def set_color(self, color): + self.color = color + + +class PulseManager: + """Manages multiple pulsing cell effects.""" + + # Preset colors + OBJECTIVE = (0, 255, 100) # Green - goals, exits + WARNING = (255, 100, 0) # Orange - danger zones + TREASURE = (255, 215, 0) # Gold - loot + MAGIC = (150, 50, 255) # Purple - magical + HEAL = (100, 200, 255) # Light blue - healing + DAMAGE = (255, 50, 50) # Red - damage zones + + def __init__(self, grid): + self.grid = grid + self.pulses = {} # (x, y) -> PulsingCell + + def add(self, x, y, color, period=1.0, max_alpha=180): + """Add a pulsing cell.""" + key = (x, y) + if key in self.pulses: + self.pulses[key].stop() + + pulse = PulsingCell(self.grid, x, y, color, period, max_alpha) + pulse.start() + self.pulses[key] = pulse + return pulse + + def remove(self, x, y): + """Stop and remove a pulsing cell.""" + key = (x, y) + if key in self.pulses: + self.pulses[key].stop() + del self.pulses[key] + + def clear(self): + """Stop all pulsing cells.""" + for pulse in self.pulses.values(): + pulse.stop() + self.pulses.clear() + + def has_pulse(self, x, y): + """Check if a cell is pulsing.""" + return (x, y) in self.pulses + + # Convenience methods with presets + def mark_objective(self, x, y): + """Mark a cell as an objective.""" + return self.add(x, y, self.OBJECTIVE, period=1.5) + + def mark_warning(self, x, y): + """Mark a cell as dangerous.""" + return self.add(x, y, self.WARNING, period=0.8, max_alpha=150) + + def mark_treasure(self, x, y): + """Mark a cell with treasure.""" + return self.add(x, y, self.TREASURE, period=1.2) + + def mark_magic(self, x, y): + """Mark a magical cell.""" + return self.add(x, y, self.MAGIC, period=2.0) + + +# Usage +scene = mcrfpy.Scene("demo") +mcrfpy.current_scene = scene + +grid = mcrfpy.Grid(grid_size=(20, 16), pos=(0, 0), size=(512, 384)) +scene.children.append(grid) + +pulses = PulseManager(grid) + +# Mark various cells +pulses.mark_objective(15, 10) # Exit door +pulses.mark_treasure(8, 5) # Chest location +pulses.mark_warning(12, 12) # Trap + +# When player reaches objective +pulses.remove(15, 10) + +# Clear all when changing levels +pulses.clear() diff --git a/tests/snippets/135_effects_color_pulse_4.py b/tests/snippets/135_effects_color_pulse_4.py new file mode 100644 index 0000000..33e898e --- /dev/null +++ b/tests/snippets/135_effects_color_pulse_4.py @@ -0,0 +1,106 @@ +# mcrf: objects=[Color,ColorLayer,Grid,Scene,Timer] verified=0.2.8-dev status=ok +import mcrfpy +import math + +class AreaPulse: + """Pulse a rectangular or circular area.""" + + def __init__(self, grid): + self.grid = grid + self.cells = [] # list of (x, y) + self.is_pulsing = False + self.timer = None + self.color_layer = self._get_or_create_layer() + + def _get_or_create_layer(self): + existing = self.grid.layer("area_pulse") + if existing is not None: + return existing + layer = mcrfpy.ColorLayer(z_index=1, name="area_pulse") + self.grid.add_layer(layer) + return layer + + def set_rect(self, x, y, width, height, color): + """Set a rectangular area to pulse.""" + self.color = color + self.cells = [ + (x + dx, y + dy) + for dy in range(height) + for dx in range(width) + ] + + def set_circle(self, center_x, center_y, radius, color): + """Set a circular area to pulse.""" + self.color = color + self.cells = [ + (center_x + dx, center_y + dy) + for dy in range(-radius, radius + 1) + for dx in range(-radius, radius + 1) + if dx * dx + dy * dy <= radius * radius + ] + + def _write(self, alpha): + r, g, b = self.color + for (x, y) in self.cells: + self.color_layer.set((x, y), mcrfpy.Color(r, g, b, alpha)) + + def pulse_once(self, duration=0.5, max_alpha=180): + """Single pulse of the area: fade in, then fade out.""" + half = duration / 2.0 + state = {"t0": None} + + def tick(timer, runtime): + if state["t0"] is None: + state["t0"] = runtime + t = (runtime - state["t0"]) / 1000.0 + if t >= duration: + self._write(0) + timer.stop() + return + phase = t / half + frac = phase if phase <= 1.0 else 2.0 - phase + self._write(int(max_alpha * frac)) + + mcrfpy.Timer(f"area_once_{id(self)}", tick, 33) + + def start_continuous(self, period=1.0, max_alpha=150): + """Start continuous pulsing of the area.""" + self.is_pulsing = True + state = {"t0": None} + + def tick(timer, runtime): + if not self.is_pulsing: + return + if state["t0"] is None: + state["t0"] = runtime + t = (runtime - state["t0"]) / 1000.0 + wave = (math.sin(2 * math.pi * t / period) + 1.0) / 2.0 + self._write(int(max_alpha * wave)) + + self.timer = mcrfpy.Timer(f"area_continuous_{id(self)}", tick, 33) + + def stop(self): + """Stop pulsing and clear.""" + self.is_pulsing = False + if self.timer is not None: + self.timer.stop() + self.timer = None + self._write(0) + + +# Usage +scene = mcrfpy.Scene("demo") +mcrfpy.current_scene = scene + +grid = mcrfpy.Grid(grid_size=(20, 16), pos=(0, 0), size=(512, 384)) +scene.children.append(grid) + +area = AreaPulse(grid) + +# Highlight a room +area.set_rect(5, 5, 4, 4, (100, 200, 255)) +area.start_continuous(period=2.0) + +# Or highlight explosion radius +area.set_circle(10, 10, 3, (255, 100, 0)) +area.pulse_once(duration=0.8) diff --git a/tests/snippets/136_effects_color_pulse_5.py b/tests/snippets/136_effects_color_pulse_5.py new file mode 100644 index 0000000..3cf70ab --- /dev/null +++ b/tests/snippets/136_effects_color_pulse_5.py @@ -0,0 +1,66 @@ +# mcrf: objects=[Color,ColorLayer,Grid,Scene,Timer] verified=0.2.8-dev status=ok +import mcrfpy + +def ripple_effect(grid, center_x, center_y, color, max_radius=5, duration=1.0): + """ + Create an expanding ripple effect. + + Args: + grid: Grid with color layer + center_x, center_y: Ripple origin + color: RGB tuple + max_radius: Maximum ripple size + duration: Total animation time + """ + layer = grid.layer("ripple") + if layer is None: + layer = mcrfpy.ColorLayer(z_index=1, name="ripple") + grid.add_layer(layer) + + step_duration = duration / max_radius + r, g, b = color + + for radius in range(max_radius + 1): + # Cells approximately on the ring edge at this radius + ring_cells = [] + for dy in range(-radius, radius + 1): + for dx in range(-radius, radius + 1): + dist_sq = dx * dx + dy * dy + if radius * radius - radius <= dist_sq <= radius * radius + radius: + ring_cells.append((center_x + dx, center_y + dy)) + + fade_duration = step_duration * 2 + + def make_ring_animator(cells): + state = {"t0": None} + + def tick(timer, runtime): + if state["t0"] is None: + state["t0"] = runtime + t = (runtime - state["t0"]) / 1000.0 + frac = max(0.0, 1.0 - t / fade_duration) + alpha = int(200 * frac) + for cx, cy in cells: + layer.set((cx, cy), mcrfpy.Color(r, g, b, alpha)) + if t >= fade_duration: + for cx, cy in cells: + layer.set((cx, cy), mcrfpy.Color(r, g, b, 0)) + timer.stop() + return tick + + def start_ring(timer, runtime, cells=ring_cells): + mcrfpy.Timer(f"ripple_fade_{id(cells)}", make_ring_animator(cells), 33) + timer.stop() + + delay = int(radius * step_duration * 1000) + mcrfpy.Timer(f"ripple_{radius}", start_ring, max(delay, 1), once=True) + + +# Usage +scene = mcrfpy.Scene("demo") +mcrfpy.current_scene = scene + +grid = mcrfpy.Grid(grid_size=(24, 24), pos=(0, 0), size=(512, 512)) +scene.children.append(grid) + +ripple_effect(grid, 12, 12, (100, 200, 255), max_radius=6, duration=0.8) diff --git a/tests/snippets/137_effects_damage_flash_1.py b/tests/snippets/137_effects_damage_flash_1.py new file mode 100644 index 0000000..206ba7e --- /dev/null +++ b/tests/snippets/137_effects_damage_flash_1.py @@ -0,0 +1,40 @@ +# mcrf: objects=[Color,ColorLayer,Entity,Grid,Scene,Timer] verified=0.2.8-dev status=ok +import mcrfpy + +scene = mcrfpy.Scene("demo") +mcrfpy.current_scene = scene + +grid = mcrfpy.Grid(grid_size=(16, 12), pos=(0, 0), size=(640, 480)) +scene.children.append(grid) + +for y in range(12): + for x in range(16): + grid.at(x, y).tilesprite = 48 + +# One ColorLayer, reused for every flash effect on this grid. +color_layer = mcrfpy.ColorLayer(z_index=1, name="flash") +grid.add_layer(color_layer) + +def flash_cell(x, y, color, duration=0.3, steps=10): + """Flash a grid cell with a color overlay that fades out over `duration` seconds.""" + r, g, b = color + step_ms = max(1, int(duration * 1000 / steps)) + + def do_fade(timer, runtime, step=[0]): + step[0] += 1 + alpha = max(0, int(200 * (1 - step[0] / steps))) + color_layer.set((x, y), mcrfpy.Color(r, g, b, alpha)) + if step[0] >= steps: + timer.stop() + + color_layer.set((x, y), mcrfpy.Color(r, g, b, 200)) + mcrfpy.Timer(f"flash_{x}_{y}", do_fade, step_ms) + +def damage_flash(entity, duration=0.3): + """Flash red on the cell an entity occupies.""" + flash_cell(entity.cell_x, entity.cell_y, (255, 0, 0), duration) + +# Usage +player = mcrfpy.Entity(grid_pos=(8, 6), texture=mcrfpy.default_texture) +grid.entities.append(player) +damage_flash(player) diff --git a/tests/snippets/138_effects_damage_flash_2.py b/tests/snippets/138_effects_damage_flash_2.py new file mode 100644 index 0000000..f39f60b --- /dev/null +++ b/tests/snippets/138_effects_damage_flash_2.py @@ -0,0 +1,95 @@ +# mcrf: objects=[Color,ColorLayer,Entity,Grid,Scene,Timer] verified=0.2.8-dev status=ok +import mcrfpy + +scene = mcrfpy.Scene("demo") +mcrfpy.current_scene = scene + +grid = mcrfpy.Grid(grid_size=(16, 12), pos=(0, 0), size=(640, 480)) +scene.children.append(grid) + +for y in range(12): + for x in range(16): + grid.at(x, y).tilesprite = 48 + +class DamageEffects: + """Manages visual damage feedback effects.""" + + # Color presets + DAMAGE_RED = (255, 50, 50) + HEAL_GREEN = (50, 255, 50) + POISON_PURPLE = (150, 50, 200) + FIRE_ORANGE = (255, 150, 50) + ICE_BLUE = (100, 200, 255) + + def __init__(self, grid): + self.grid = grid + self.color_layer = None + self._setup_color_layer() + + def _setup_color_layer(self): + """Ensure grid has a color layer for effects.""" + self.color_layer = mcrfpy.ColorLayer(z_index=1, name="flash") + self.grid.add_layer(self.color_layer) + + def flash_entity(self, entity, color, duration=0.3): + """Flash the cell an entity currently occupies.""" + self.flash_cell(entity.cell_x, entity.cell_y, color, duration) + + def flash_cell(self, x, y, color, duration=0.3, steps=10): + """Flash a specific grid cell, fading out over `duration` seconds.""" + if not self.color_layer: + return + + r, g, b = color + step_ms = max(1, int(duration * 1000 / steps)) + + def do_fade(timer, runtime, step=[0]): + step[0] += 1 + alpha = max(0, int(180 * (1 - step[0] / steps))) + self.color_layer.set((x, y), mcrfpy.Color(r, g, b, alpha)) + if step[0] >= steps: + timer.stop() + + self.color_layer.set((x, y), mcrfpy.Color(r, g, b, 180)) + mcrfpy.Timer(f"flash_{x}_{y}", do_fade, step_ms) + + def damage(self, entity, amount, duration=0.3): + """Standard damage flash.""" + self.flash_entity(entity, self.DAMAGE_RED, duration) + + def heal(self, entity, amount, duration=0.4): + """Healing effect - green flash.""" + self.flash_entity(entity, self.HEAL_GREEN, duration) + + def poison(self, entity, duration=0.5): + """Poison damage - purple flash.""" + self.flash_entity(entity, self.POISON_PURPLE, duration) + + def fire(self, entity, duration=0.3): + """Fire damage - orange flash.""" + self.flash_entity(entity, self.FIRE_ORANGE, duration) + + def ice(self, entity, duration=0.4): + """Ice damage - blue flash.""" + self.flash_entity(entity, self.ICE_BLUE, duration) + + def area_damage(self, center_x, center_y, radius, color, duration=0.4): + """Flash all cells in a radius.""" + for dy in range(-radius, radius + 1): + for dx in range(-radius, radius + 1): + if dx * dx + dy * dy <= radius * radius: + self.flash_cell(center_x + dx, center_y + dy, color, duration) + +# Setup +effects = DamageEffects(grid) + +player = mcrfpy.Entity(grid_pos=(5, 5), texture=mcrfpy.default_texture) +grid.entities.append(player) +enemy = mcrfpy.Entity(grid_pos=(9, 5), texture=mcrfpy.default_texture) +grid.entities.append(enemy) + +# Usage examples +effects.damage(player, 10) # Red flash +effects.heal(player, 5) # Green flash +effects.poison(enemy) # Purple flash +effects.area_damage(5, 5, 3, effects.FIRE_ORANGE) # Area effect diff --git a/tests/snippets/139_effects_damage_flash_3.py b/tests/snippets/139_effects_damage_flash_3.py new file mode 100644 index 0000000..c582926 --- /dev/null +++ b/tests/snippets/139_effects_damage_flash_3.py @@ -0,0 +1,36 @@ +# mcrf: objects=[Color,ColorLayer,Entity,Grid,Scene,Timer] verified=0.2.8-dev status=ok +import mcrfpy + +scene = mcrfpy.Scene("demo") +mcrfpy.current_scene = scene + +grid = mcrfpy.Grid(grid_size=(16, 12), pos=(0, 0), size=(640, 480)) +scene.children.append(grid) + +for y in range(12): + for x in range(16): + grid.at(x, y).tilesprite = 48 + +color_layer = mcrfpy.ColorLayer(z_index=1, name="flash") +grid.add_layer(color_layer) + +def multi_flash(grid, color_layer, x, y, color, flashes=3, flash_duration=0.1): + """Flash a cell multiple times for emphasis.""" + r, g, b = color + delay = 0 + + for i in range(flashes): + def do_flash(timer, runtime, fr=r, fg=g, fb=b): + color_layer.set((x, y), mcrfpy.Color(fr, fg, fb, 200)) + mcrfpy.Timer(f"multiflash_clear_{x}_{y}_{timer.name}", + lambda t, rt: color_layer.set((x, y), mcrfpy.Color(fr, fg, fb, 0)), + int(flash_duration * 1000), once=True) + + mcrfpy.Timer(f"multiflash_{x}_{y}_{i}", do_flash, max(1, int(delay * 1000)), once=True) + delay += flash_duration * 1.5 # Gap between flashes + +enemy = mcrfpy.Entity(grid_pos=(9, 5), texture=mcrfpy.default_texture) +grid.entities.append(enemy) + +# Usage for critical hit +multi_flash(grid, color_layer, enemy.cell_x, enemy.cell_y, (255, 255, 0), flashes=3) diff --git a/tests/snippets/140_effects_floating_text.py b/tests/snippets/140_effects_floating_text.py new file mode 100644 index 0000000..c1407cc --- /dev/null +++ b/tests/snippets/140_effects_floating_text.py @@ -0,0 +1,30 @@ +# mcrf: objects=[Caption,Color,Easing,Scene,Timer] verified=0.2.8-dev status=ok +import mcrfpy + +def show_damage(scene, x, y, amount, color=(255, 50, 50)): + """Display floating damage number that rises and fades.""" + # Create the caption + caption = mcrfpy.Caption(text=str(amount), x=x, y=y) + caption.fill_color = mcrfpy.Color(color[0], color[1], color[2], 255) + caption.font_size = 24 + scene.children.append(caption) + + # Animate upward movement + caption.animate("y", float(y - 50), 0.8, mcrfpy.Easing.EASE_OUT) + + # Animate fade out + caption.animate("opacity", 0.0, 0.8, mcrfpy.Easing.EASE_IN) + + # Remove caption after animation completes + def cleanup(timer, runtime): + try: + scene.children.remove(caption) + except ValueError: + pass + + mcrfpy.Timer("cleanup_damage", cleanup, 850, once=True) + +# Usage +game_scene = mcrfpy.Scene("game") +mcrfpy.current_scene = game_scene +show_damage(game_scene, 400, 300, 42) diff --git a/tests/snippets/141_effects_path_animation.py b/tests/snippets/141_effects_path_animation.py new file mode 100644 index 0000000..9725273 --- /dev/null +++ b/tests/snippets/141_effects_path_animation.py @@ -0,0 +1,88 @@ +# mcrf: objects=[Easing,Entity,Grid,Scene] verified=0.2.8-dev status=ok +import mcrfpy + +class PathAnimator: + """Animate an entity along a series of waypoints.""" + + def __init__(self, entity, path, step_duration=0.2, easing=None): + """ + Args: + entity: The Entity to animate + path: List of (x, y) waypoints in grid coordinates + step_duration: Seconds per step + easing: mcrfpy.Easing value (default: EASE_IN_OUT) + """ + self.entity = entity + self.path = path + self.step_duration = step_duration + self.easing = easing if easing is not None else mcrfpy.Easing.EASE_IN_OUT + self.index = 0 + self.on_complete = None + self.on_step = None + + def start(self): + """Begin path animation from current position.""" + self.index = 0 + self._animate_next() + + def _animate_next(self): + """Animate to the next waypoint.""" + if self.index >= len(self.path): + # Path complete + if self.on_complete: + self.on_complete(self) + return + + target_x, target_y = self.path[self.index] + + # Create animations for this step + def on_step_complete(target, prop, value): + # Callback for step completion + if self.on_step: + self.on_step(self, self.index) + self.index += 1 + self._animate_next() + + # Animate X (draw_x/draw_y are the tile-coordinate properties used + # for smooth movement between grid cells; compare against draw_pos, + # not entity.x/entity.y, which are pixel coordinates) + pos = self.entity.draw_pos + if pos.x != target_x: + self.entity.animate("draw_x", float(target_x), self.step_duration, + self.easing, callback=on_step_complete) + # Animate Y + elif pos.y != target_y: + self.entity.animate("draw_y", float(target_y), self.step_duration, + self.easing, callback=on_step_complete) + else: + # Already at position, skip to next + self.index += 1 + self._animate_next() + + +scene = mcrfpy.Scene("path_animation_demo") +mcrfpy.current_scene = scene + +grid = mcrfpy.Grid( + grid_size=(16, 12), + texture=mcrfpy.default_texture, + pos=(0, 0), + size=(1024, 768), + zoom=4.0 +) +scene.children.append(grid) + +for y in range(12): + for x in range(16): + cell = grid.at(x, y) + cell.tilesprite = 48 + cell.walkable = True + +enemy = mcrfpy.Entity(grid_pos=(5, 5), texture=mcrfpy.default_texture, sprite_index=84) +grid.entities.append(enemy) + +# Usage +path = [(5, 5), (5, 8), (8, 8), (8, 5)] # Square patrol route +animator = PathAnimator(enemy, path, step_duration=0.3) +animator.on_complete = lambda a: print("Patrol complete!") +animator.start() diff --git a/tests/snippets/142_effects_scene_transitions_1.py b/tests/snippets/142_effects_scene_transitions_1.py new file mode 100644 index 0000000..14f9625 --- /dev/null +++ b/tests/snippets/142_effects_scene_transitions_1.py @@ -0,0 +1,12 @@ +# mcrf: objects=[Frame,Scene] verified=0.2.8-dev status=ok +import mcrfpy + +# Create scenes, then switch between them with activate() +menu_scene = mcrfpy.Scene("menu") +game_scene = mcrfpy.Scene("game") + +# Instant switch (default) -- also settable via mcrfpy.current_scene = scene +menu_scene.activate() +game_scene.activate() + +game_scene.children.append(mcrfpy.Frame(pos=(0, 0), size=(1024, 768))) diff --git a/tests/snippets/143_effects_scene_transitions_2.py b/tests/snippets/143_effects_scene_transitions_2.py new file mode 100644 index 0000000..99f2f2e --- /dev/null +++ b/tests/snippets/143_effects_scene_transitions_2.py @@ -0,0 +1,77 @@ +# mcrf: objects=[Color,Easing,Frame,Scene,Timer] verified=0.2.8-dev status=ok +import mcrfpy + + +class FadeTransition: + """Custom fade transition with callback support.""" + + def __init__(self, current_scene, target_scene): + """ + Args: + current_scene: The mcrfpy.Scene object to transition from + target_scene: The mcrfpy.Scene object to transition to + """ + self.current_scene = current_scene + self.target_scene = target_scene + self.overlay = None + + def _create_overlay(self, scene): + """Create a full-screen black overlay in the given scene.""" + self.overlay = mcrfpy.Frame(pos=(0, 0), size=(1024, 768)) + self.overlay.fill_color = mcrfpy.Color(0, 0, 0, 0) + self.overlay.z_index = 9999 # On top of everything + scene.children.append(self.overlay) + + def fade_out_in(self, duration=0.5, on_complete=None): + """ + Fade to black, switch scene, fade in. + + Args: + duration: Total transition time (half for out, half for in) + on_complete: Callback when transition finishes + """ + half_duration = duration / 2 + + # Create overlay in current scene + self._create_overlay(self.current_scene) + + # Fade to black + self.overlay.animate("opacity", 1.0, half_duration, mcrfpy.Easing.EASE_IN) + + # Switch scene and fade in + def switch_and_fade_in(timer, runtime): + # Switch to new scene + self.target_scene.activate() + + # Create overlay in new scene + self._create_overlay(self.target_scene) + self.overlay.opacity = 1.0 # Start fully opaque + + # Fade in + self.overlay.animate("opacity", 0.0, half_duration, mcrfpy.Easing.EASE_OUT) + + # Cleanup and callback + def cleanup(timer, runtime): + self.target_scene.children.remove(self.overlay) + if on_complete: + on_complete() + + mcrfpy.Timer("fade_cleanup", cleanup, int(half_duration * 1000) + 50, once=True) + + mcrfpy.Timer("fade_switch", switch_and_fade_in, int(half_duration * 1000), once=True) + + +# Usage +menu_scene = mcrfpy.Scene("menu") +game_scene = mcrfpy.Scene("game") + + +def on_transition_complete(): + print("Transition complete, game starting!") + + +# Start on menu, then transition to game +menu_scene.activate() + +transition = FadeTransition(menu_scene, game_scene) +transition.fade_out_in(duration=0.8, on_complete=on_transition_complete) diff --git a/tests/snippets/144_effects_scene_transitions_3.py b/tests/snippets/144_effects_scene_transitions_3.py new file mode 100644 index 0000000..92ef2e2 --- /dev/null +++ b/tests/snippets/144_effects_scene_transitions_3.py @@ -0,0 +1,57 @@ +# mcrf: objects=[Color,Easing,Frame,Scene,Timer] verified=0.2.8-dev status=ok +import mcrfpy + + +def flash_transition(current_scene, target_scene, color=(255, 255, 255), duration=0.4): + """ + Flash to a color, then switch scene. + + Args: + current_scene: The mcrfpy.Scene object to transition from + target_scene: The mcrfpy.Scene object to transition to + color: Flash color RGB tuple + duration: Total transition time + """ + # Create colored overlay in current scene + overlay = mcrfpy.Frame(pos=(0, 0), size=(1024, 768)) + overlay.fill_color = mcrfpy.Color(color[0], color[1], color[2], 0) + overlay.z_index = 9999 + current_scene.children.append(overlay) + + quarter = duration / 4 + + # Flash in (fast) + overlay.animate("opacity", 1.0, quarter, mcrfpy.Easing.EASE_OUT) + + # Hold, then switch and fade out + def switch_scene(timer, runtime): + target_scene.activate() + + # Create overlay in new scene + new_overlay = mcrfpy.Frame(pos=(0, 0), size=(1024, 768)) + new_overlay.fill_color = mcrfpy.Color(color[0], color[1], color[2], 255) + new_overlay.z_index = 9999 + target_scene.children.append(new_overlay) + + # Fade out (slower) + new_overlay.animate("opacity", 0.0, duration / 2, mcrfpy.Easing.EASE_IN) + + # Cleanup + def cleanup(timer, runtime): + target_scene.children.remove(new_overlay) + + mcrfpy.Timer("flash_cleanup", cleanup, int(duration * 500) + 50, once=True) + + mcrfpy.Timer("flash_switch", switch_scene, int(quarter * 2000), once=True) + + +# Usage +game_scene = mcrfpy.Scene("game") +next_level_scene = mcrfpy.Scene("next_level") +death_scene = mcrfpy.Scene("death_screen") + +mcrfpy.current_scene = game_scene +game_scene.children.append(mcrfpy.Frame(pos=(0, 0), size=(1024, 768))) + +flash_transition(game_scene, next_level_scene, color=(255, 255, 255), duration=0.5) # White flash +flash_transition(game_scene, death_scene, color=(255, 0, 0), duration=0.8) # Red flash diff --git a/tests/snippets/145_effects_scene_transitions_4.py b/tests/snippets/145_effects_scene_transitions_4.py new file mode 100644 index 0000000..cd5e8c8 --- /dev/null +++ b/tests/snippets/145_effects_scene_transitions_4.py @@ -0,0 +1,90 @@ +# mcrf: objects=[Color,Easing,Frame,Scene,Timer] verified=0.2.8-dev status=ok +import mcrfpy + + +def wipe_transition(current_scene, target_scene, direction="right", duration=0.5, color=(0, 0, 0)): + """ + Wipe transition from one edge. + + Args: + current_scene: The mcrfpy.Scene object to transition from + target_scene: The mcrfpy.Scene object to transition to + direction: "left", "right", "up", "down" + duration: Total transition time + color: Wipe color + """ + half = duration / 2 + + # Set up wipe overlay based on direction + if direction == "right": + overlay = mcrfpy.Frame(pos=(0, 0), size=(0, 768)) + anim_prop = "w" + anim_target = 1024 + elif direction == "left": + overlay = mcrfpy.Frame(pos=(1024, 0), size=(0, 768)) + anim_prop = "x" # Move left edge + anim_target = 0 + elif direction == "down": + overlay = mcrfpy.Frame(pos=(0, 0), size=(1024, 0)) + anim_prop = "h" + anim_target = 768 + else: # up + overlay = mcrfpy.Frame(pos=(0, 768), size=(1024, 0)) + anim_prop = "y" + anim_target = 0 + + overlay.fill_color = mcrfpy.Color(color[0], color[1], color[2], 255) + overlay.z_index = 9999 + current_scene.children.append(overlay) + + # Wipe in + overlay.animate(anim_prop, float(anim_target), half, mcrfpy.Easing.EASE_IN_OUT) + + # Switch and wipe out + def switch_and_wipe(timer, runtime): + target_scene.activate() + + # Create covering overlay + if direction == "right": + new_overlay = mcrfpy.Frame(pos=(0, 0), size=(1024, 768)) + new_anim_prop = "x" + new_anim_target = 1024 + elif direction == "left": + new_overlay = mcrfpy.Frame(pos=(0, 0), size=(1024, 768)) + new_anim_prop = "w" + new_anim_target = 0 + elif direction == "down": + new_overlay = mcrfpy.Frame(pos=(0, 0), size=(1024, 768)) + new_anim_prop = "y" + new_anim_target = -768 + else: + new_overlay = mcrfpy.Frame(pos=(0, 0), size=(1024, 768)) + new_anim_prop = "h" + new_anim_target = 0 + + new_overlay.fill_color = mcrfpy.Color(color[0], color[1], color[2], 255) + new_overlay.z_index = 9999 + target_scene.children.append(new_overlay) + + # Wipe out + new_overlay.animate(new_anim_prop, float(new_anim_target), half, mcrfpy.Easing.EASE_IN_OUT) + + # Cleanup + def cleanup(timer, runtime): + target_scene.children.remove(new_overlay) + + mcrfpy.Timer("wipe_cleanup", cleanup, int(half * 1000) + 50, once=True) + + mcrfpy.Timer("wipe_switch", switch_and_wipe, int(half * 1000), once=True) + + +# Usage +game_scene = mcrfpy.Scene("game") +next_level = mcrfpy.Scene("next_level") +menu_scene = mcrfpy.Scene("menu") + +mcrfpy.current_scene = game_scene +game_scene.children.append(mcrfpy.Frame(pos=(0, 0), size=(1024, 768))) + +wipe_transition(game_scene, next_level, direction="right", duration=0.6) +wipe_transition(game_scene, menu_scene, direction="down", duration=0.4) diff --git a/tests/snippets/146_effects_scene_transitions_5.py b/tests/snippets/146_effects_scene_transitions_5.py new file mode 100644 index 0000000..51a6e50 --- /dev/null +++ b/tests/snippets/146_effects_scene_transitions_5.py @@ -0,0 +1,159 @@ +# mcrf: objects=[Color,Easing,Frame,Scene,Timer] verified=0.2.8-dev status=ok +import mcrfpy + + +class TransitionManager: + """Manages scene transitions with multiple effect types.""" + + def __init__(self, screen_width=1024, screen_height=768): + self.width = screen_width + self.height = screen_height + self.is_transitioning = False + self.current_scene = None # Track active scene + + def go_to(self, target_scene, effect="fade", duration=0.5, **kwargs): + """ + Transition to a scene with the specified effect. + + Args: + target_scene: The mcrfpy.Scene object to transition to + effect: "fade", "flash", "wipe", "instant" + duration: Transition duration + **kwargs: Effect-specific options (color, direction) + """ + if self.is_transitioning: + return + + if self.current_scene is None: + # No current scene tracked, just activate target + target_scene.activate() + self.current_scene = target_scene + return + + self.is_transitioning = True + + if effect == "instant": + target_scene.activate() + self.current_scene = target_scene + self.is_transitioning = False + + elif effect == "fade": + color = kwargs.get("color", (0, 0, 0)) + self._fade(target_scene, duration, color) + + elif effect == "flash": + color = kwargs.get("color", (255, 255, 255)) + self._flash(target_scene, duration, color) + + elif effect == "wipe": + direction = kwargs.get("direction", "right") + color = kwargs.get("color", (0, 0, 0)) + self._wipe(target_scene, duration, direction, color) + + def _fade(self, target_scene, duration, color): + half = duration / 2 + + overlay = mcrfpy.Frame(pos=(0, 0), size=(self.width, self.height)) + overlay.fill_color = mcrfpy.Color(color[0], color[1], color[2], 0) + overlay.z_index = 9999 + self.current_scene.children.append(overlay) + + overlay.animate("opacity", 1.0, half, mcrfpy.Easing.EASE_IN) + + def phase2(timer, runtime): + target_scene.activate() + + new_overlay = mcrfpy.Frame(pos=(0, 0), size=(self.width, self.height)) + new_overlay.fill_color = mcrfpy.Color(color[0], color[1], color[2], 255) + new_overlay.z_index = 9999 + target_scene.children.append(new_overlay) + + new_overlay.animate("opacity", 0.0, half, mcrfpy.Easing.EASE_OUT) + + def cleanup(timer, runtime): + target_scene.children.remove(new_overlay) + self.current_scene = target_scene + self.is_transitioning = False + + mcrfpy.Timer("fade_done", cleanup, int(half * 1000) + 50, once=True) + + mcrfpy.Timer("fade_switch", phase2, int(half * 1000), once=True) + + def _flash(self, target_scene, duration, color): + quarter = duration / 4 + + overlay = mcrfpy.Frame(pos=(0, 0), size=(self.width, self.height)) + overlay.fill_color = mcrfpy.Color(color[0], color[1], color[2], 0) + overlay.z_index = 9999 + self.current_scene.children.append(overlay) + + overlay.animate("opacity", 1.0, quarter, mcrfpy.Easing.EASE_OUT) + + def phase2(timer, runtime): + target_scene.activate() + + new_overlay = mcrfpy.Frame(pos=(0, 0), size=(self.width, self.height)) + new_overlay.fill_color = mcrfpy.Color(color[0], color[1], color[2], 255) + new_overlay.z_index = 9999 + target_scene.children.append(new_overlay) + + new_overlay.animate("opacity", 0.0, duration / 2, mcrfpy.Easing.EASE_IN) + + def cleanup(timer, runtime): + target_scene.children.remove(new_overlay) + self.current_scene = target_scene + self.is_transitioning = False + + mcrfpy.Timer("flash_done", cleanup, int(duration * 500) + 50, once=True) + + mcrfpy.Timer("flash_switch", phase2, int(quarter * 2000), once=True) + + def _wipe(self, target_scene, duration, direction, color): + # Simplified wipe - right direction only for brevity + half = duration / 2 + + overlay = mcrfpy.Frame(pos=(0, 0), size=(0, self.height)) + overlay.fill_color = mcrfpy.Color(color[0], color[1], color[2], 255) + overlay.z_index = 9999 + self.current_scene.children.append(overlay) + + overlay.animate("w", float(self.width), half, mcrfpy.Easing.EASE_IN_OUT) + + def phase2(timer, runtime): + target_scene.activate() + + new_overlay = mcrfpy.Frame(pos=(0, 0), size=(self.width, self.height)) + new_overlay.fill_color = mcrfpy.Color(color[0], color[1], color[2], 255) + new_overlay.z_index = 9999 + target_scene.children.append(new_overlay) + + new_overlay.animate("x", float(self.width), half, mcrfpy.Easing.EASE_IN_OUT) + + def cleanup(timer, runtime): + target_scene.children.remove(new_overlay) + self.current_scene = target_scene + self.is_transitioning = False + + mcrfpy.Timer("wipe_done", cleanup, int(half * 1000) + 50, once=True) + + mcrfpy.Timer("wipe_switch", phase2, int(half * 1000), once=True) + + +# Usage +# Create scenes +menu_scene = mcrfpy.Scene("menu") +game_scene = mcrfpy.Scene("game") +options_scene = mcrfpy.Scene("options") +next_level_scene = mcrfpy.Scene("next_level") + +# Create transition manager and set initial scene +transitions = TransitionManager() +transitions.current_scene = menu_scene +menu_scene.activate() +menu_scene.children.append(mcrfpy.Frame(pos=(0, 0), size=(1024, 768))) + +# Various transition styles +transitions.go_to(game_scene, effect="fade", duration=0.5) +transitions.go_to(menu_scene, effect="flash", color=(255, 255, 255), duration=0.4) +transitions.go_to(next_level_scene, effect="wipe", direction="right", duration=0.6) +transitions.go_to(options_scene, effect="instant") diff --git a/tests/snippets/147_effects_screen_shake_1.py b/tests/snippets/147_effects_screen_shake_1.py new file mode 100644 index 0000000..65d2c12 --- /dev/null +++ b/tests/snippets/147_effects_screen_shake_1.py @@ -0,0 +1,32 @@ +# mcrf: objects=[Easing,Frame,Scene,Timer] verified=0.2.8-dev status=ok +import mcrfpy + +scene = mcrfpy.Scene("demo") +mcrfpy.current_scene = scene + +def screen_shake(frame, intensity=5, duration=0.2): + """ + Shake a frame/container by animating its position. + + Args: + frame: The UI Frame to shake (often a container for all game elements) + intensity: Maximum pixel offset + duration: Total shake duration in seconds + """ + original_x = frame.x + original_y = frame.y + + # Quick shake to offset position + frame.animate("x", float(original_x + intensity), duration / 4, mcrfpy.Easing.EASE_OUT) + + # Schedule return to center + def return_to_center(timer, runtime): + frame.animate("x", float(original_x), duration / 2, mcrfpy.Easing.EASE_IN_OUT) + + mcrfpy.Timer("shake_return", return_to_center, int(duration * 250), once=True) + +# Usage - wrap your game content in a Frame +game_container = mcrfpy.Frame(pos=(0, 0), size=(1024, 768)) +scene.children.append(game_container) +# ... add game elements to game_container.children ... +screen_shake(game_container, intensity=8, duration=0.3) diff --git a/tests/snippets/148_effects_screen_shake_2.py b/tests/snippets/148_effects_screen_shake_2.py new file mode 100644 index 0000000..bc649f8 --- /dev/null +++ b/tests/snippets/148_effects_screen_shake_2.py @@ -0,0 +1,47 @@ +# mcrf: objects=[Easing,Frame,Scene,Timer] verified=0.2.8-dev status=ok +import mcrfpy +import random + +scene = mcrfpy.Scene("demo") +mcrfpy.current_scene = scene + +game_container = mcrfpy.Frame(pos=(0, 0), size=(1024, 768)) +scene.children.append(game_container) + +def screen_shake_multi(frame, intensity=5, duration=0.3, shakes=4): + """ + Shake frame in multiple random directions. + + Args: + frame: The UI Frame to shake + intensity: Maximum pixel offset + duration: Total shake duration + shakes: Number of shake movements + """ + original_x = frame.x + original_y = frame.y + shake_interval = duration / shakes + + for i in range(shakes): + # Decreasing intensity over time + current_intensity = intensity * (1 - i / shakes) + + # Random offset direction + offset_x = random.uniform(-current_intensity, current_intensity) + offset_y = random.uniform(-current_intensity, current_intensity) + + def do_shake(timer, runtime, ox=offset_x, oy=offset_y, si=shake_interval): + frame.animate("x", float(original_x + ox), si * 0.8, mcrfpy.Easing.EASE_OUT) + frame.animate("y", float(original_y + oy), si * 0.8, mcrfpy.Easing.EASE_OUT) + + mcrfpy.Timer(f"shake_{i}", do_shake, max(1, int(i * shake_interval * 1000)), once=True) + + # Return to original position + def reset(timer, runtime): + frame.animate("x", float(original_x), shake_interval, mcrfpy.Easing.EASE_OUT) + frame.animate("y", float(original_y), shake_interval, mcrfpy.Easing.EASE_OUT) + + mcrfpy.Timer("shake_reset", reset, int(duration * 1000), once=True) + +# Usage +screen_shake_multi(game_container, intensity=10, duration=0.4, shakes=6) diff --git a/tests/snippets/149_effects_screen_shake_3.py b/tests/snippets/149_effects_screen_shake_3.py new file mode 100644 index 0000000..1aeae0e --- /dev/null +++ b/tests/snippets/149_effects_screen_shake_3.py @@ -0,0 +1,51 @@ +# mcrf: objects=[Easing,Grid,Scene,Timer] verified=0.2.8-dev status=ok +import mcrfpy +import random + +scene = mcrfpy.Scene("demo") +mcrfpy.current_scene = scene + +grid = mcrfpy.Grid( + grid_size=(16, 12), + texture=mcrfpy.default_texture, + pos=(0, 0), + size=(1024, 768), + zoom=4.0 +) +scene.children.append(grid) + +def camera_shake(grid, intensity=8, duration=0.3, shakes=4): + """ + Shake the camera by animating grid.center. + + Args: + grid: The Grid object + intensity: Shake intensity in pixels + duration: Total duration + shakes: Number of shake movements + """ + original_center_x = grid.center[0] + original_center_y = grid.center[1] + shake_interval = duration / shakes + + for i in range(shakes): + decay = 1 - (i / shakes) # Decreasing intensity + offset_x = random.uniform(-intensity, intensity) * decay + offset_y = random.uniform(-intensity, intensity) * decay + + def do_shake(timer, runtime, ox=offset_x, oy=offset_y, si=shake_interval, + cx=original_center_x, cy=original_center_y): + grid.animate("center_x", cx + ox, si * 0.8, mcrfpy.Easing.EASE_OUT) + grid.animate("center_y", cy + oy, si * 0.8, mcrfpy.Easing.EASE_OUT) + + mcrfpy.Timer(f"cam_shake_{i}", do_shake, max(1, int(i * shake_interval * 1000)), once=True) + + # Return to original + def reset(timer, runtime): + grid.animate("center_x", float(original_center_x), shake_interval, mcrfpy.Easing.EASE_OUT) + grid.animate("center_y", float(original_center_y), shake_interval, mcrfpy.Easing.EASE_OUT) + + mcrfpy.Timer("cam_reset", reset, int(duration * 1000), once=True) + +# Usage +camera_shake(grid, intensity=12, duration=0.25) diff --git a/tests/snippets/150_grid_interaction_patterns_1.py b/tests/snippets/150_grid_interaction_patterns_1.py new file mode 100644 index 0000000..1f88fb8 --- /dev/null +++ b/tests/snippets/150_grid_interaction_patterns_1.py @@ -0,0 +1,35 @@ +# mcrf: objects=[Color,ColorLayer,Entity,Grid,Scene,TileLayer] verified=0.2.8-dev status=ok +import mcrfpy + +# Scene setup +scene = mcrfpy.Scene("game") +ui = scene.children + +# Load tileset texture (use your own atlas in a real game) +texture = mcrfpy.default_texture + +# Create layers +terrain = mcrfpy.TileLayer(name="terrain", z_index=-2, texture=texture) +highlight = mcrfpy.ColorLayer(name="highlight", z_index=-1) +overlay = mcrfpy.ColorLayer(name="overlay", z_index=1) + +# Create grid with layers +grid = mcrfpy.Grid( + grid_size=(20, 15), + pos=(50, 50), + size=(640, 480), + layers=[terrain, highlight, overlay] +) +grid.fill_color = mcrfpy.Color(20, 20, 30) +ui.append(grid) + +# Create player entity +player = mcrfpy.Entity(grid_pos=(10, 7), sprite_index=0) +grid.entities.append(player) + +# Wire up events (patterns below fill these in) +# grid.on_cell_click = ... +# grid.on_cell_enter = ... +# grid.on_cell_exit = ... + +mcrfpy.current_scene = scene diff --git a/tests/snippets/151_grid_interaction_patterns_2.py b/tests/snippets/151_grid_interaction_patterns_2.py new file mode 100644 index 0000000..49df780 --- /dev/null +++ b/tests/snippets/151_grid_interaction_patterns_2.py @@ -0,0 +1,42 @@ +# mcrf: objects=[Color,ColorLayer,Entity,Grid,Scene,TileLayer] verified=0.2.8-dev status=ok +import mcrfpy + +# Scene setup +scene = mcrfpy.Scene("game") +ui = scene.children + +texture = mcrfpy.default_texture + +terrain = mcrfpy.TileLayer(name="terrain", z_index=-2, texture=texture) +highlight = mcrfpy.ColorLayer(name="highlight", z_index=-1) +overlay = mcrfpy.ColorLayer(name="overlay", z_index=1) + +grid = mcrfpy.Grid( + grid_size=(20, 15), + pos=(50, 50), + size=(640, 480), + layers=[terrain, highlight, overlay] +) +grid.fill_color = mcrfpy.Color(20, 20, 30) +ui.append(grid) + +player = mcrfpy.Entity(grid_pos=(10, 7), sprite_index=0) +grid.entities.append(player) + +mcrfpy.current_scene = scene + +# --- Cell Hover Highlighting --- +current_highlight = [None] + +def on_cell_enter(cell_pos): + x, y = int(cell_pos.x), int(cell_pos.y) + highlight.set((x, y), mcrfpy.Color(255, 255, 255, 40)) + current_highlight[0] = (x, y) + +def on_cell_exit(cell_pos): + x, y = int(cell_pos.x), int(cell_pos.y) + highlight.set((x, y), mcrfpy.Color(0, 0, 0, 0)) + current_highlight[0] = None + +grid.on_cell_enter = on_cell_enter +grid.on_cell_exit = on_cell_exit diff --git a/tests/snippets/152_grid_interaction_patterns_3.py b/tests/snippets/152_grid_interaction_patterns_3.py new file mode 100644 index 0000000..57179fe --- /dev/null +++ b/tests/snippets/152_grid_interaction_patterns_3.py @@ -0,0 +1,47 @@ +# mcrf: objects=[Color,ColorLayer,Entity,Grid,InputState,MouseButton,Scene,TileLayer] verified=0.2.8-dev status=ok +import mcrfpy + +scene = mcrfpy.Scene("game") +ui = scene.children + +texture = mcrfpy.default_texture +terrain = mcrfpy.TileLayer(name="terrain", z_index=-2, texture=texture) +highlight = mcrfpy.ColorLayer(name="highlight", z_index=-1) +overlay = mcrfpy.ColorLayer(name="overlay", z_index=1) + +grid = mcrfpy.Grid( + grid_size=(20, 15), + pos=(50, 50), + size=(640, 480), + layers=[terrain, highlight, overlay] +) +grid.fill_color = mcrfpy.Color(20, 20, 30) +ui.append(grid) + +player = mcrfpy.Entity(grid_pos=(10, 7), sprite_index=0) +grid.entities.append(player) + +mcrfpy.current_scene = scene + +# --- Cell Click Actions --- +def show_cell_info(x, y, point): + print(f"Cell ({x}, {y}): walkable={point.walkable}") + +def on_cell_click(cell_pos, button, action): + x, y = int(cell_pos.x), int(cell_pos.y) + point = grid.at(x, y) + + if button == mcrfpy.MouseButton.LEFT and action == mcrfpy.InputState.PRESSED: + if point.walkable: + player.grid_x = x + player.grid_y = y + + elif button == mcrfpy.MouseButton.RIGHT and action == mcrfpy.InputState.PRESSED: + # Inspect cell + show_cell_info(x, y, point) + + elif button == mcrfpy.MouseButton.MIDDLE and action == mcrfpy.InputState.PRESSED: + # Toggle walkability (level editor) + point.walkable = not point.walkable + +grid.on_cell_click = on_cell_click diff --git a/tests/snippets/153_grid_interaction_patterns_4.py b/tests/snippets/153_grid_interaction_patterns_4.py new file mode 100644 index 0000000..420b713 --- /dev/null +++ b/tests/snippets/153_grid_interaction_patterns_4.py @@ -0,0 +1,48 @@ +# mcrf: objects=[Color,ColorLayer,Entity,Grid,InputState,Key,Scene,TileLayer] verified=0.2.8-dev status=ok +import mcrfpy + +scene = mcrfpy.Scene("game") +ui = scene.children + +texture = mcrfpy.default_texture +terrain = mcrfpy.TileLayer(name="terrain", z_index=-2, texture=texture) +highlight = mcrfpy.ColorLayer(name="highlight", z_index=-1) +overlay = mcrfpy.ColorLayer(name="overlay", z_index=1) + +grid = mcrfpy.Grid( + grid_size=(20, 15), + pos=(50, 50), + size=(640, 480), + layers=[terrain, highlight, overlay] +) +grid.fill_color = mcrfpy.Color(20, 20, 30) +ui.append(grid) + +player = mcrfpy.Entity(grid_pos=(10, 7), sprite_index=0) +grid.entities.append(player) + +mcrfpy.current_scene = scene + +# --- WASD Movement --- +move_map = { + mcrfpy.Key.W: (0, -1), + mcrfpy.Key.A: (-1, 0), + mcrfpy.Key.S: (0, 1), + mcrfpy.Key.D: (1, 0), +} + +def handle_key(key, action): + if action != mcrfpy.InputState.PRESSED: + return + if key in move_map: + dx, dy = move_map[key] + new_x = int(player.grid_x + dx) + new_y = int(player.grid_y + dy) + grid_w, grid_h = player.grid.grid_size + if 0 <= new_x < grid_w and 0 <= new_y < grid_h: + point = player.grid.at(new_x, new_y) + if point.walkable: + player.grid_x = new_x + player.grid_y = new_y + +scene.on_key = handle_key diff --git a/tests/snippets/154_grid_interaction_patterns_5.py b/tests/snippets/154_grid_interaction_patterns_5.py new file mode 100644 index 0000000..3049be2 --- /dev/null +++ b/tests/snippets/154_grid_interaction_patterns_5.py @@ -0,0 +1,65 @@ +# mcrf: objects=[Caption,Color,ColorLayer,Entity,Frame,Grid,InputState,MouseButton,Scene,TileLayer] verified=0.2.8-dev status=ok +import mcrfpy + +scene = mcrfpy.Scene("game") +ui = scene.children + +texture = mcrfpy.default_texture +terrain = mcrfpy.TileLayer(name="terrain", z_index=-2, texture=texture) +highlight = mcrfpy.ColorLayer(name="highlight", z_index=-1) +overlay = mcrfpy.ColorLayer(name="overlay", z_index=1) + +grid = mcrfpy.Grid( + grid_size=(20, 15), + pos=(50, 50), + size=(640, 480), + layers=[terrain, highlight, overlay] +) +grid.fill_color = mcrfpy.Color(20, 20, 30) +ui.append(grid) + +player = mcrfpy.Entity(grid_pos=(10, 7), sprite_index=0) +grid.entities.append(player) + +mcrfpy.current_scene = scene + +# --- Tile Inspector Panel --- +inspector = mcrfpy.Frame(pos=(700, 50), size=(200, 150), + fill_color=mcrfpy.Color(30, 30, 40, 230)) +inspector.outline = 1 +inspector.outline_color = mcrfpy.Color(80, 80, 100) +inspector.visible = False +ui.append(inspector) + +title = mcrfpy.Caption(text="Cell Info", pos=(10, 8)) +title.fill_color = mcrfpy.Color(180, 180, 200) +inspector.children.append(title) + +info_lines = [] +for i in range(4): + line = mcrfpy.Caption(text="", pos=(10, 30 + i * 20)) + line.fill_color = mcrfpy.Color(160, 160, 180) + inspector.children.append(line) + info_lines.append(line) + +def on_cell_click(cell_pos, button, action): + if button == mcrfpy.MouseButton.LEFT and action == mcrfpy.InputState.PRESSED: + x, y = int(cell_pos.x), int(cell_pos.y) + point = grid.at(x, y) + + title.text = f"Cell ({x}, {y})" + info_lines[0].text = f"Walkable: {point.walkable}" + info_lines[1].text = f"Transparent: {point.transparent}" + + entities_here = [e for e in grid.entities + if int(e.grid_x) == x and int(e.grid_y) == y] + info_lines[2].text = f"Entities: {len(entities_here)}" + + if entities_here: + info_lines[3].text = f" {entities_here[0].name or 'unnamed'}" + else: + info_lines[3].text = "" + + inspector.visible = True + +grid.on_cell_click = on_cell_click diff --git a/tests/snippets/155_grid_interaction_patterns_6.py b/tests/snippets/155_grid_interaction_patterns_6.py new file mode 100644 index 0000000..20b4ead --- /dev/null +++ b/tests/snippets/155_grid_interaction_patterns_6.py @@ -0,0 +1,50 @@ +# mcrf: objects=[Color,ColorLayer,Entity,Grid,InputState,MouseButton,Scene,TileLayer] verified=0.2.8-dev status=ok +import mcrfpy + +scene = mcrfpy.Scene("game") +ui = scene.children + +texture = mcrfpy.default_texture +terrain = mcrfpy.TileLayer(name="terrain", z_index=-2, texture=texture) +highlight = mcrfpy.ColorLayer(name="highlight", z_index=-1) +overlay = mcrfpy.ColorLayer(name="overlay", z_index=1) + +grid = mcrfpy.Grid( + grid_size=(20, 15), + pos=(50, 50), + size=(640, 480), + layers=[terrain, highlight, overlay] +) +grid.fill_color = mcrfpy.Color(20, 20, 30) +ui.append(grid) + +player = mcrfpy.Entity(grid_pos=(10, 7), sprite_index=0) +grid.entities.append(player) + +mcrfpy.current_scene = scene + +# --- Entity Selection --- +selected = [None] + +def select_entity(entity): + # Clear previous selection + if selected[0]: + ex, ey = int(selected[0].grid_x), int(selected[0].grid_y) + overlay.set((ex, ey), mcrfpy.Color(0, 0, 0, 0)) + + selected[0] = entity + + if entity: + overlay.set((int(entity.grid_x), int(entity.grid_y)), + mcrfpy.Color(255, 200, 0, 80)) + +def on_cell_click(cell_pos, button, action): + if button == mcrfpy.MouseButton.LEFT and action == mcrfpy.InputState.PRESSED: + x, y = int(cell_pos.x), int(cell_pos.y) + for entity in grid.entities: + if int(entity.grid_x) == x and int(entity.grid_y) == y: + select_entity(entity) + return + select_entity(None) + +grid.on_cell_click = on_cell_click diff --git a/tests/snippets/156_grid_interaction_patterns_7.py b/tests/snippets/156_grid_interaction_patterns_7.py new file mode 100644 index 0000000..bbbb468 --- /dev/null +++ b/tests/snippets/156_grid_interaction_patterns_7.py @@ -0,0 +1,60 @@ +# mcrf: objects=[Color,ColorLayer,Entity,Grid,Scene,TileLayer] verified=0.2.8-dev status=ok +import mcrfpy + +scene = mcrfpy.Scene("game") +ui = scene.children + +texture = mcrfpy.default_texture +terrain = mcrfpy.TileLayer(name="terrain", z_index=-2, texture=texture) +highlight = mcrfpy.ColorLayer(name="highlight", z_index=-1) +overlay = mcrfpy.ColorLayer(name="overlay", z_index=1) + +grid = mcrfpy.Grid( + grid_size=(20, 15), + pos=(50, 50), + size=(640, 480), + layers=[terrain, highlight, overlay] +) +grid.fill_color = mcrfpy.Color(20, 20, 30) +ui.append(grid) + +player = mcrfpy.Entity(grid_pos=(10, 7), sprite_index=0) +grid.entities.append(player) + +mcrfpy.current_scene = scene + +# --- Path Preview --- +current_path_cells = [] + +def show_path_to(target_x, target_y): + global current_path_cells + # Clear previous path + for cx, cy in current_path_cells: + highlight.set((cx, cy), mcrfpy.Color(0, 0, 0, 0)) + current_path_cells = [] + + # Calculate path + path = grid.find_path( + (int(player.grid_x), int(player.grid_y)), + (target_x, target_y) + ) + if not path: + return + + # Draw path cells + i = 0 + while len(path) > 0: + step = path.walk() + x, y = int(step.x), int(step.y) + alpha = max(30, 100 - (i * 5)) + highlight.set((x, y), mcrfpy.Color(100, 200, 255, alpha)) + current_path_cells.append((x, y)) + i += 1 + +def on_cell_enter(cell_pos): + show_path_to(int(cell_pos.x), int(cell_pos.y)) + +grid.on_cell_enter = on_cell_enter + +# Exercise the pattern once so the layer content isn't dead code +show_path_to(15, 12) diff --git a/tests/snippets/157_grid_cell_highlighting_1.py b/tests/snippets/157_grid_cell_highlighting_1.py new file mode 100644 index 0000000..ed45995 --- /dev/null +++ b/tests/snippets/157_grid_cell_highlighting_1.py @@ -0,0 +1,30 @@ +# mcrf: objects=[Color,ColorLayer,Grid,Scene] verified=0.2.8-dev status=ok +import mcrfpy + +scene = mcrfpy.Scene("demo") +mcrfpy.current_scene = scene + +grid = mcrfpy.Grid( + grid_size=(16, 12), + texture=mcrfpy.default_texture, + pos=(0, 0), + size=(1024, 768), + zoom=4.0 +) +scene.children.append(grid) + +# Create a highlight layer (above tiles, below entities) +highlight_layer = mcrfpy.ColorLayer(name="highlight", z_index=1) +grid.add_layer(highlight_layer) + +def highlight_cells(cells, color): + """Highlight a list of (x, y) coordinates.""" + highlight_layer.fill(mcrfpy.Color(0, 0, 0, 0)) + for x, y in cells: + highlight_layer.set((x, y), color) + +def clear_highlights(): + """Remove all highlighting.""" + highlight_layer.fill(mcrfpy.Color(0, 0, 0, 0)) + +highlight_cells([(1, 1), (2, 1), (3, 1)], mcrfpy.Color(50, 100, 255, 100)) diff --git a/tests/snippets/158_grid_cell_highlighting_2.py b/tests/snippets/158_grid_cell_highlighting_2.py new file mode 100644 index 0000000..6470977 --- /dev/null +++ b/tests/snippets/158_grid_cell_highlighting_2.py @@ -0,0 +1,69 @@ +# mcrf: objects=[Color,ColorLayer,Grid,Scene] verified=0.2.8-dev status=ok +import mcrfpy + +scene = mcrfpy.Scene("demo") +mcrfpy.current_scene = scene + +grid = mcrfpy.Grid( + grid_size=(16, 12), + texture=mcrfpy.default_texture, + pos=(0, 0), + size=(1024, 768), + zoom=4.0 +) +scene.children.append(grid) + + +class HighlightManager: + """Manage multiple highlight layers.""" + + COLORS = { + 'move': mcrfpy.Color(50, 100, 255, 80), # Blue + 'attack': mcrfpy.Color(255, 50, 50, 100), # Red + 'heal': mcrfpy.Color(50, 255, 50, 100), # Green + 'danger': mcrfpy.Color(255, 100, 0, 120), # Orange + 'select': mcrfpy.Color(255, 255, 50, 150), # Yellow + 'path': mcrfpy.Color(255, 255, 255, 80), # White + } + + def __init__(self, grid): + self.grid = grid + self.layer = mcrfpy.ColorLayer(name="highlight", z_index=1) + grid.add_layer(self.layer) + self.highlights = {} # category -> set of cells + + def add(self, category, cells): + """Add highlights for a category.""" + self.highlights[category] = set(cells) + self._refresh() + + def remove(self, category): + """Remove highlights for a category.""" + if category in self.highlights: + del self.highlights[category] + self._refresh() + + def clear(self): + """Clear all highlights.""" + self.highlights = {} + self.layer.fill(mcrfpy.Color(0, 0, 0, 0)) + + def _refresh(self): + """Redraw all highlights with proper layering.""" + self.layer.fill(mcrfpy.Color(0, 0, 0, 0)) + + # Draw in priority order (later categories draw on top) + priority = ['move', 'attack', 'heal', 'danger', 'path', 'select'] + + for category in priority: + if category in self.highlights: + color = self.COLORS.get(category, mcrfpy.Color(128, 128, 128, 100)) + for x, y in self.highlights[category]: + self.layer.set((x, y), color) + + +# Usage example +highlights = HighlightManager(grid) +highlights.add('move', [(2, 2), (3, 2), (4, 2)]) +highlights.add('attack', [(5, 5)]) +highlights.remove('attack') diff --git a/tests/snippets/159_grid_dijkstra_1.py b/tests/snippets/159_grid_dijkstra_1.py new file mode 100644 index 0000000..32638a9 --- /dev/null +++ b/tests/snippets/159_grid_dijkstra_1.py @@ -0,0 +1,61 @@ +# mcrf: objects=[Caption,Color,ColorLayer,Entity,Grid,Scene] verified=0.2.8-dev status=ok +import mcrfpy + +scene = mcrfpy.Scene("demo") +mcrfpy.current_scene = scene + +grid = mcrfpy.Grid( + grid_size=(16, 12), + texture=mcrfpy.default_texture, + pos=(0, 0), + size=(1024, 768), + zoom=4.0 +) +scene.children.append(grid) + +# Color layer to visualize the distance field +dist_layer = mcrfpy.ColorLayer(z_index=1, name="distances") +grid.add_layer(dist_layer) + +# Floor tiles with a wall down the middle +for y in range(12): + for x in range(16): + cell = grid.at(x, y) + cell.tilesprite = 48 + cell.walkable = True + +for i in range(5): + grid.at(8, 2 + i).tilesprite = 1 + grid.at(8, 2 + i).walkable = False + +player = mcrfpy.Entity(grid_pos=(3, 6), texture=mcrfpy.default_texture, sprite_index=84) +grid.entities.append(player) + +enemy = mcrfpy.Entity(grid_pos=(13, 6), texture=mcrfpy.default_texture, sprite_index=112) +grid.entities.append(enemy) + + +def move_enemy_toward_player(grid, enemy, player): + """Move enemy one step toward the player.""" + dijkstra = grid.get_dijkstra_map(player.grid_pos) + step = dijkstra.step_from(enemy.grid_pos) + if step is not None: + enemy.grid_pos = step + + +# Visualize the distance field rooted at the player +dijkstra = grid.get_dijkstra_map(player.grid_pos) +for y in range(12): + for x in range(16): + if grid.at(x, y).walkable: + dist = dijkstra.distance((x, y)) + if dist is not None: + intensity = max(0, 255 - int(dist * 20)) + dist_layer.set((x, y), mcrfpy.Color(intensity, intensity, 255, 180)) + +move_enemy_toward_player(grid, enemy, player) + +status = mcrfpy.Caption(text="Dijkstra distance map (darker = farther)", pos=(320, 720)) +status.outline = 2 +status.outline_color = mcrfpy.Color(0, 0, 0) +scene.children.append(status) diff --git a/tests/snippets/160_grid_dijkstra_2.py b/tests/snippets/160_grid_dijkstra_2.py new file mode 100644 index 0000000..d8d0003 --- /dev/null +++ b/tests/snippets/160_grid_dijkstra_2.py @@ -0,0 +1,43 @@ +# mcrf: objects=[Caption,Color,Entity,Grid,Scene] verified=0.2.8-dev status=ok +import mcrfpy + +scene = mcrfpy.Scene("demo") +mcrfpy.current_scene = scene + +grid = mcrfpy.Grid( + grid_size=(16, 12), + texture=mcrfpy.default_texture, + pos=(0, 0), + size=(1024, 768), + zoom=4.0 +) +scene.children.append(grid) + +for y in range(12): + for x in range(16): + cell = grid.at(x, y) + cell.tilesprite = 48 + cell.walkable = True + +threat = mcrfpy.Entity(grid_pos=(8, 6), texture=mcrfpy.default_texture, sprite_index=84) +grid.entities.append(threat) + +prey = mcrfpy.Entity(grid_pos=(9, 6), texture=mcrfpy.default_texture, sprite_index=112) +grid.entities.append(prey) + + +def ai_flee(grid, entity, threat_x, threat_y): + """Move entity away from threat using an inverted Dijkstra map.""" + toward_threat = grid.get_dijkstra_map((threat_x, threat_y)) + away_from_threat = toward_threat.invert() + step = away_from_threat.descent_step(entity.grid_pos) + if step is not None: + entity.grid_pos = step + + +ai_flee(grid, prey, threat.grid_pos[0], threat.grid_pos[1]) + +status = mcrfpy.Caption(text="Flee behavior via DijkstraMap.invert()", pos=(320, 720)) +status.outline = 2 +status.outline_color = mcrfpy.Color(0, 0, 0) +scene.children.append(status) diff --git a/tests/snippets/161_grid_dijkstra_heightmap_1.py b/tests/snippets/161_grid_dijkstra_heightmap_1.py new file mode 100644 index 0000000..9af2bc9 --- /dev/null +++ b/tests/snippets/161_grid_dijkstra_heightmap_1.py @@ -0,0 +1,21 @@ +# mcrf: objects=[Grid,Scene] verified=0.2.8-dev status=ok +import mcrfpy + +scene = mcrfpy.Scene("quickstart") +mcrfpy.current_scene = scene + +# Create a grid with walkable cells +grid = mcrfpy.Grid(grid_size=(20, 20)) +for y in range(20): + for x in range(20): + grid.at((x, y)).walkable = True +scene.children.append(grid) + +# Get dijkstra map from player position +dijkstra = grid.get_dijkstra_map((10, 10)) + +# Convert to heightmap +hmap = dijkstra.to_heightmap() + +# Use the heights +player_distance = hmap[(5, 5)] # Distance from (5,5) to (10,10) diff --git a/tests/snippets/162_grid_dijkstra_heightmap_2.py b/tests/snippets/162_grid_dijkstra_heightmap_2.py new file mode 100644 index 0000000..ab7ba6e --- /dev/null +++ b/tests/snippets/162_grid_dijkstra_heightmap_2.py @@ -0,0 +1,54 @@ +# mcrf: objects=[Color,ColorLayer,Grid,Scene] verified=0.2.8-dev status=ok +import mcrfpy + +scene = mcrfpy.Scene("dijkstra_gradient") +mcrfpy.current_scene = scene + +grid = mcrfpy.Grid(grid_size=(20, 20)) +for y in range(20): + for x in range(20): + grid.at((x, y)).walkable = True +scene.children.append(grid) + +color_layer = mcrfpy.ColorLayer(z_index=1, name="dijkstra_vis") +grid.add_layer(color_layer) + + +def visualize_dijkstra(grid, color_layer, source): + """Render dijkstra distances as a color gradient.""" + dijkstra = grid.get_dijkstra_map(source) + hmap = dijkstra.to_heightmap(unreachable=-1.0) + + width, height = int(grid.grid_size[0]), int(grid.grid_size[1]) + + # Find max distance for normalization + max_dist = 0 + for y in range(height): + for x in range(width): + d = hmap[(x, y)] + if d > max_dist and d != -1.0: + max_dist = d + + # Color each cell + for y in range(height): + for x in range(width): + dist = hmap[(x, y)] + + if dist == -1.0: + # Unreachable - dark red + color = mcrfpy.Color(60, 0, 0) + elif dist == 0: + # Source - bright yellow + color = mcrfpy.Color(255, 255, 0) + else: + # Gradient from green (near) to blue (far) + t = dist / max_dist + r = 0 + g = int(255 * (1 - t)) + b = int(255 * t) + color = mcrfpy.Color(r, g, b) + + color_layer.set((x, y), color) + + +visualize_dijkstra(grid, color_layer, (10, 10)) diff --git a/tests/snippets/163_grid_dungeon_generator.py b/tests/snippets/163_grid_dungeon_generator.py new file mode 100644 index 0000000..c460498 --- /dev/null +++ b/tests/snippets/163_grid_dungeon_generator.py @@ -0,0 +1,206 @@ +# mcrf: objects=[Entity,Grid,Scene] verified=0.2.8-dev status=ok +import mcrfpy +import random + +# Tile indices (adjust for your tileset) +TILE_FLOOR = 0 +TILE_WALL = 1 +TILE_DOOR = 2 +TILE_STAIRS_DOWN = 3 +TILE_STAIRS_UP = 4 + + +class Room: + """Represents a rectangular room in the dungeon.""" + + def __init__(self, x, y, width, height): + self.x = x + self.y = y + self.width = width + self.height = height + + @property + def center(self): + """Return the center point of the room.""" + return ( + self.x + self.width // 2, + self.y + self.height // 2 + ) + + @property + def inner(self): + """Return the inner area (excluding walls).""" + return ( + self.x + 1, self.y + 1, + self.width - 2, self.height - 2 + ) + + def intersects(self, other, padding=1): + """Check if this room overlaps another (with padding).""" + return ( + self.x - padding < other.x + other.width + padding and + self.x + self.width + padding > other.x - padding and + self.y - padding < other.y + other.height + padding and + self.y + self.height + padding > other.y - padding + ) + + +class DungeonGenerator: + """Procedural dungeon generator with rooms and corridors.""" + + def __init__(self, grid, seed=None): + self.grid = grid + self.grid_w, self.grid_h = int(grid.grid_size.x), int(grid.grid_size.y) + self.rooms = [] + + if seed is not None: + random.seed(seed) + + def generate(self, room_count=8, min_room=4, max_room=10): + """Generate a complete dungeon level.""" + self.rooms = [] + + # Fill with walls + self._fill_walls() + + # Place rooms + attempts = 0 + max_attempts = room_count * 10 + + while len(self.rooms) < room_count and attempts < max_attempts: + attempts += 1 + + # Random room size + w = random.randint(min_room, max_room) + h = random.randint(min_room, max_room) + + # Random position (leaving border) + x = random.randint(1, self.grid_w - w - 2) + y = random.randint(1, self.grid_h - h - 2) + + room = Room(x, y, w, h) + + # Check overlap + if not any(room.intersects(r) for r in self.rooms): + self._carve_room(room) + + # Connect to previous room + if self.rooms: + self._dig_corridor(self.rooms[-1].center, room.center) + + self.rooms.append(room) + + # Place stairs + if len(self.rooms) >= 2: + self._place_stairs() + + return self.rooms + + def _fill_walls(self): + """Fill the entire grid with wall tiles.""" + for x in range(self.grid_w): + for y in range(self.grid_h): + point = self.grid.at(x, y) + point.tilesprite = TILE_WALL + point.walkable = False + point.transparent = False + + def _carve_room(self, room): + """Carve out a room, making it walkable.""" + for x in range(room.x, room.x + room.width): + for y in range(room.y, room.y + room.height): + self._set_floor(x, y) + + def _set_floor(self, x, y): + """Set a single tile as floor.""" + if 0 <= x < self.grid_w and 0 <= y < self.grid_h: + point = self.grid.at(x, y) + point.tilesprite = TILE_FLOOR + point.walkable = True + point.transparent = True + + def _dig_corridor(self, start, end): + """Dig an L-shaped corridor between two points.""" + x1, y1 = start + x2, y2 = end + + # Randomly choose horizontal-first or vertical-first + if random.random() < 0.5: + # Horizontal then vertical + self._dig_horizontal(x1, x2, y1) + self._dig_vertical(y1, y2, x2) + else: + # Vertical then horizontal + self._dig_vertical(y1, y2, x1) + self._dig_horizontal(x1, x2, y2) + + def _dig_horizontal(self, x1, x2, y): + """Dig a horizontal tunnel.""" + for x in range(min(x1, x2), max(x1, x2) + 1): + self._set_floor(x, y) + + def _dig_vertical(self, y1, y2, x): + """Dig a vertical tunnel.""" + for y in range(min(y1, y2), max(y1, y2) + 1): + self._set_floor(x, y) + + def _place_stairs(self): + """Place stairs in first and last rooms.""" + # Stairs up in first room + start_room = self.rooms[0] + sx, sy = start_room.center + point = self.grid.at(sx, sy) + point.tilesprite = TILE_STAIRS_UP + + # Stairs down in last room + end_room = self.rooms[-1] + ex, ey = end_room.center + point = self.grid.at(ex, ey) + point.tilesprite = TILE_STAIRS_DOWN + + return (sx, sy), (ex, ey) + + def get_spawn_point(self): + """Get a good spawn point for the player.""" + if self.rooms: + return self.rooms[0].center + return (self.grid_w // 2, self.grid_h // 2) + + def get_random_floor(self): + """Get a random walkable floor tile.""" + floors = [] + for x in range(self.grid_w): + for y in range(self.grid_h): + if self.grid.at(x, y).walkable: + floors.append((x, y)) + return random.choice(floors) if floors else None + + +# Setup +scene = mcrfpy.Scene("dungeon") +mcrfpy.current_scene = scene + +texture = mcrfpy.default_texture +grid = mcrfpy.Grid(grid_size=(60, 40), texture=texture, pos=(0, 0), size=(960, 640)) +scene.children.append(grid) + +# Generate dungeon +generator = DungeonGenerator(grid, seed=42) +rooms = generator.generate(room_count=10) + +# Place player at spawn +spawn = generator.get_spawn_point() +player = mcrfpy.Entity(spawn, texture, 64) +grid.entities.append(player) + +# Place some enemies in random rooms +for i in range(5): + pos = generator.get_random_floor() + if pos and pos != spawn: + enemy = mcrfpy.Entity(pos, texture, 80) + grid.entities.append(enemy) + +# Center camera +grid.center_camera(spawn) + +print(f"Generated dungeon with {len(rooms)} rooms") diff --git a/tests/snippets/164_grid_fog_of_war_1.py b/tests/snippets/164_grid_fog_of_war_1.py new file mode 100644 index 0000000..1a39549 --- /dev/null +++ b/tests/snippets/164_grid_fog_of_war_1.py @@ -0,0 +1,20 @@ +# mcrf: objects=[Entity,Grid,Scene] verified=0.2.8-dev status=ok +import mcrfpy + +# Create scene and grid +scene = mcrfpy.Scene("game") +mcrfpy.current_scene = scene + +texture = mcrfpy.default_texture +grid = mcrfpy.Grid(grid_size=(50, 50), texture=texture, pos=(0, 0), size=(800, 600)) +scene.children.append(grid) + +# Add player entity +player = mcrfpy.Entity(grid_pos=(25, 25), texture=texture, sprite_index=0) +grid.entities.append(player) + +# Set player as the perspective entity - the view now renders fog of war +# based on what the player has seen (unseen cells drawn black, discovered +# cells dimmed). Set to None for an omniscient (fully-lit) view. +grid.perspective = player +grid.fov_radius = 10 diff --git a/tests/snippets/165_grid_fog_of_war_2.py b/tests/snippets/165_grid_fog_of_war_2.py new file mode 100644 index 0000000..5d32fd1 --- /dev/null +++ b/tests/snippets/165_grid_fog_of_war_2.py @@ -0,0 +1,87 @@ +# mcrf: objects=[Color,ColorLayer,Entity,Grid,InputState,Key,Scene] verified=0.2.8-dev status=ok +import mcrfpy + +# Scene setup +scene = mcrfpy.Scene("dungeon") +mcrfpy.current_scene = scene + +# Load tileset +texture = mcrfpy.default_texture + +# Create grid +grid = mcrfpy.Grid(grid_size=(50, 50), texture=texture, pos=(0, 0), size=(800, 600)) +scene.children.append(grid) + +# Initialize dungeon - walls block sight and movement +for x in range(50): + for y in range(50): + point = grid.at(x, y) + if x == 0 or y == 0 or x == 49 or y == 49: + # Outer walls + point.tilesprite = 1 # Wall tile + point.walkable = False + point.transparent = False + else: + # Floor tiles + point.tilesprite = 0 # Floor tile + point.walkable = True + point.transparent = True + +# Add some pillars to test LOS blocking +pillars = [(10, 10), (20, 15), (30, 25), (15, 35)] +for px, py in pillars: + point = grid.at(px, py) + point.tilesprite = 2 # Pillar tile + point.walkable = False + point.transparent = False + +# Create player +player = mcrfpy.Entity(grid_pos=(25, 25), texture=texture, sprite_index=64) # Player sprite +grid.entities.append(player) + +# Add a custom fog of war layer bound to the player +fog_layer = mcrfpy.ColorLayer(z_index=-1, name="fog") +grid.add_layer(fog_layer) +fog_layer.apply_perspective( + entity=player, + visible=mcrfpy.Color(0, 0, 0, 0), + discovered=mcrfpy.Color(20, 20, 40, 160), + unknown=mcrfpy.Color(0, 0, 0, 255) +) + +grid.fov_radius = 8 + +def update_visibility(): + """Recompute FOV and repaint the bound fog layer.""" + player.update_visibility() + +def handle_input(key, action): + if action != mcrfpy.InputState.PRESSED: + return + + x, y = player.grid_x, player.grid_y + dx, dy = 0, 0 + + if key in (mcrfpy.Key.W, mcrfpy.Key.Up): + dy = -1 + elif key in (mcrfpy.Key.S, mcrfpy.Key.Down): + dy = 1 + elif key in (mcrfpy.Key.A, mcrfpy.Key.Left): + dx = -1 + elif key in (mcrfpy.Key.D, mcrfpy.Key.Right): + dx = 1 + + if dx != 0 or dy != 0: + new_x, new_y = x + dx, y + dy + cell = grid.at(new_x, new_y) + if cell is not None and cell.walkable: + player.grid_pos = (new_x, new_y) + update_visibility() + +scene.on_key = handle_input + +# Initial FOV calculation +update_visibility() + +# Center camera on player +grid.center_camera((player.grid_x + 0.5, player.grid_y + 0.5)) diff --git a/tests/snippets/166_grid_multi_layer_1.py b/tests/snippets/166_grid_multi_layer_1.py new file mode 100644 index 0000000..52f83ed --- /dev/null +++ b/tests/snippets/166_grid_multi_layer_1.py @@ -0,0 +1,30 @@ +# mcrf: objects=[ColorLayer,Grid,Scene,TileLayer] verified=0.2.8-dev status=ok +import mcrfpy + +scene = mcrfpy.Scene("layers") +mcrfpy.current_scene = scene + +grid = mcrfpy.Grid( + grid_size=(16, 12), + texture=mcrfpy.default_texture, + pos=(0, 0), + size=(1024, 768), + zoom=4.0 +) +scene.children.append(grid) + +TILE_FLOOR = 48 +TILE_RUG = 65 + +# Add layers with different z-indices +base_layer = mcrfpy.TileLayer(z_index=0, name="base", texture=mcrfpy.default_texture) # Base terrain +decor_layer = mcrfpy.TileLayer(z_index=1, name="decor", texture=mcrfpy.default_texture) # Decorations +effect_layer = mcrfpy.ColorLayer(z_index=2, name="effects") # Color effects +ui_layer = mcrfpy.ColorLayer(z_index=3, name="ui") # UI highlights + +for layer in (base_layer, decor_layer, effect_layer, ui_layer): + grid.add_layer(layer) + +# Set tiles on each layer +base_layer.set((5, 5), TILE_FLOOR) +decor_layer.set((5, 5), TILE_RUG) # Rug on top of floor diff --git a/tests/snippets/167_grid_multi_layer_2.py b/tests/snippets/167_grid_multi_layer_2.py new file mode 100644 index 0000000..d212954 --- /dev/null +++ b/tests/snippets/167_grid_multi_layer_2.py @@ -0,0 +1,118 @@ +# mcrf: objects=[Grid,Scene,TileLayer] verified=0.2.8-dev status=ok +import mcrfpy + +scene = mcrfpy.Scene("layers-decor") +mcrfpy.current_scene = scene + +grid = mcrfpy.Grid( + grid_size=(16, 12), + texture=mcrfpy.default_texture, + pos=(0, 0), + size=(1024, 768), + zoom=4.0 +) +scene.children.append(grid) + +for y in range(12): + for x in range(16): + grid.at(x, y).walkable = True + + +class DecorationManager: + """Manage decorative tile overlays.""" + + # Decoration categories with sprite indices + DECORATIONS = { + 'furniture': { + 'table': 100, + 'chair': 101, + 'bed': 102, + 'chest': 103, + 'bookshelf': 104, + }, + 'debris': { + 'bones': 110, + 'rubble': 111, + 'cobweb': 112, + }, + 'nature': { + 'grass_tuft': 120, + 'mushroom': 121, + 'flower': 122, + 'moss': 123, + }, + 'stains': { + 'blood': 130, + 'water': 131, + 'acid': 132, + } + } + + def __init__(self, grid): + self.grid = grid + self.layer = grid.add_layer( + mcrfpy.TileLayer(z_index=1, name="decor", texture=mcrfpy.default_texture) + ) + self.placed = {} # (x, y) -> decoration_name + + def place(self, x, y, category, name): + """Place a decoration.""" + if category in self.DECORATIONS: + if name in self.DECORATIONS[category]: + sprite_idx = self.DECORATIONS[category][name] + self.layer.set((x, y), sprite_idx) + self.placed[(x, y)] = (category, name) + + def remove(self, x, y): + """Remove a decoration.""" + self.layer.set((x, y), -1) + if (x, y) in self.placed: + del self.placed[(x, y)] + + def get(self, x, y): + """Get decoration at position.""" + return self.placed.get((x, y)) + + def scatter(self, category, count, walkable_only=True): + """Randomly scatter decorations.""" + import random + + if category not in self.DECORATIONS: + return + + names = list(self.DECORATIONS[category].keys()) + grid_w, grid_h = self.grid.grid_size + grid_w, grid_h = int(grid_w), int(grid_h) + + placed = 0 + attempts = 0 + max_attempts = count * 10 + + while placed < count and attempts < max_attempts: + attempts += 1 + + x = random.randint(0, grid_w - 1) + y = random.randint(0, grid_h - 1) + + # Check if position is valid + point = self.grid.at(x, y) + if walkable_only and not point.walkable: + continue + if (x, y) in self.placed: + continue + + name = random.choice(names) + self.place(x, y, category, name) + placed += 1 + + +# Usage +decor = DecorationManager(grid) + +# Place specific decorations +decor.place(10, 10, 'furniture', 'table') +decor.place(11, 10, 'furniture', 'chair') + +# Scatter random debris +decor.scatter('debris', 20) +decor.scatter('nature', 15) diff --git a/tests/snippets/168_headless_mode_1.py b/tests/snippets/168_headless_mode_1.py new file mode 100644 index 0000000..887053b --- /dev/null +++ b/tests/snippets/168_headless_mode_1.py @@ -0,0 +1,18 @@ +# mcrf: objects=[Easing,Frame,Scene] verified=0.2.8-dev status=ok +import mcrfpy + +scene = mcrfpy.Scene("test") +frame = mcrfpy.Frame(pos=(0, 0), size=(100, 100)) +scene.children.append(frame) +mcrfpy.current_scene = scene + +# Start animation: move x from 0 to 500 over 2 seconds +frame.animate("x", 500.0, 2.0, mcrfpy.Easing.EASE_IN_OUT) + +# Advance halfway through animation +mcrfpy.step(1.0) +print(f"Frame x: {frame.x}") # ~250 (halfway) + +# Complete the animation +mcrfpy.step(1.0) +print(f"Frame x: {frame.x}") # 500 (complete) diff --git a/tests/snippets/169_headless_mode_2.py b/tests/snippets/169_headless_mode_2.py new file mode 100644 index 0000000..c2e3e3d --- /dev/null +++ b/tests/snippets/169_headless_mode_2.py @@ -0,0 +1,20 @@ +# mcrf: objects=[Color,Frame,Scene] verified=0.2.8-dev status=ok +import mcrfpy +from mcrfpy import automation + +scene = mcrfpy.Scene("test") +ui = scene.children + +frame = mcrfpy.Frame(pos=(100, 100), size=(200, 200), + fill_color=mcrfpy.Color(255, 0, 0)) +ui.append(frame) +mcrfpy.current_scene = scene + +# Screenshot captures current state immediately (no timer needed) +automation.screenshot("/tmp/red_frame.png") + +# Change to green +frame.fill_color = mcrfpy.Color(0, 255, 0) + +# Next screenshot shows green +automation.screenshot("/tmp/green_frame.png") diff --git a/tests/snippets/170_procgen_cellular_caves.py b/tests/snippets/170_procgen_cellular_caves.py new file mode 100644 index 0000000..b3fdbe4 --- /dev/null +++ b/tests/snippets/170_procgen_cellular_caves.py @@ -0,0 +1,184 @@ +# mcrf: objects=[Entity,Grid,Scene] verified=0.2.8-dev status=ok +import mcrfpy +import random + + +def generate_caves(grid, fill_chance=0.45, iterations=5): + """Generate organic caves using cellular automata.""" + width, height = int(grid.grid_size[0]), int(grid.grid_size[1]) + + # Step 1: Random noise + for y in range(height): + for x in range(width): + cell = grid.at((x, y)) + # Border is always wall + if x == 0 or x == width-1 or y == 0 or y == height-1: + cell.walkable = False + cell.tilesprite = 1 # Wall + else: + is_wall = random.random() < fill_chance + cell.walkable = not is_wall + cell.tilesprite = 1 if is_wall else 0 + + # Step 2: Smooth with cellular automata rules + for _ in range(iterations): + smooth_caves(grid) + + return grid + +def smooth_caves(grid): + """Apply one iteration of cellular automata smoothing.""" + width, height = int(grid.grid_size[0]), int(grid.grid_size[1]) + + # Create a copy of current state + walls = [[not grid.at((x, y)).walkable + for x in range(width)] + for y in range(height)] + + for y in range(1, height - 1): + for x in range(1, width - 1): + # Count wall neighbors (including diagonals) + wall_count = 0 + for dy in [-1, 0, 1]: + for dx in [-1, 0, 1]: + if dx == 0 and dy == 0: + continue + if walls[y + dy][x + dx]: + wall_count += 1 + + cell = grid.at((x, y)) + # Rule: >=5 walls = become wall, <=3 walls = become floor + if wall_count >= 5: + cell.walkable = False + cell.tilesprite = 1 + elif wall_count <= 3: + cell.walkable = True + cell.tilesprite = 0 + + +def find_largest_cave(grid): + """Find the largest connected walkable region.""" + width, height = int(grid.grid_size[0]), int(grid.grid_size[1]) + visited = set() + regions = [] + + def flood_fill(start_x, start_y): + """Flood fill from a point, return all connected cells.""" + cells = [] + stack = [(start_x, start_y)] + + while stack: + x, y = stack.pop() + if (x, y) in visited: + continue + if x < 0 or x >= width or y < 0 or y >= height: + continue + if not grid.at((x, y)).walkable: + continue + + visited.add((x, y)) + cells.append((x, y)) + + stack.extend([(x+1, y), (x-1, y), (x, y+1), (x, y-1)]) + + return cells + + # Find all regions + for y in range(height): + for x in range(width): + if (x, y) not in visited and grid.at((x, y)).walkable: + region = flood_fill(x, y) + if region: + regions.append(region) + + # Keep only the largest + if not regions: + return [] + + largest = max(regions, key=len) + + # Fill in smaller regions as walls + for region in regions: + if region is not largest: + for x, y in region: + cell = grid.at((x, y)) + cell.walkable = False + cell.tilesprite = 1 + + return largest + + +def find_cave_features(grid, spawn_point): + """Find dead ends, wide areas, and chokepoints.""" + width, height = int(grid.grid_size[0]), int(grid.grid_size[1]) + + dead_ends = [] # Cells with only one walkable neighbor + wide_areas = [] # Cells with 6+ walkable neighbors + chokepoints = [] # Cells where path width is narrow + + for y in range(1, height - 1): + for x in range(1, width - 1): + if not grid.at((x, y)).walkable: + continue + + # Count walkable neighbors + walkable_neighbors = sum( + 1 for dy in [-1, 0, 1] + for dx in [-1, 0, 1] + if not (dx == 0 and dy == 0) + and grid.at((x + dx, y + dy)).walkable + ) + + if walkable_neighbors <= 1: + dead_ends.append((x, y)) + elif walkable_neighbors >= 6: + wide_areas.append((x, y)) + elif walkable_neighbors == 2: + chokepoints.append((x, y)) + + return { + 'dead_ends': dead_ends, # Good for treasure, secrets + 'wide_areas': wide_areas, # Good for boss arenas + 'chokepoints': chokepoints # Good for traps, doors + } + + +# Setup +scene = mcrfpy.Scene("caves") +scene.activate() + +# Create grid +texture = mcrfpy.default_texture +grid = mcrfpy.Grid(grid_size=(60, 40), texture=texture, + pos=(0, 0), size=(960, 640)) +scene.children.append(grid) + +# Generate caves +generate_caves(grid, fill_chance=0.45, iterations=5) + +# Find largest connected region +largest = find_largest_cave(grid) +print(f"Largest cave has {len(largest)} cells") + +# Find features +spawn = largest[0] # Start at first cell of largest region +features = find_cave_features(grid, spawn) +print(f"Found {len(features['dead_ends'])} dead ends") +print(f"Found {len(features['wide_areas'])} wide areas") + +# Place player at spawn +player = mcrfpy.Entity(spawn, texture, sprite_index=64) +grid.entities.append(player) + +# Place treasure at dead ends +for i, pos in enumerate(features['dead_ends'][:5]): + treasure = mcrfpy.Entity(pos, texture, sprite_index=32) + grid.entities.append(treasure) + +# Place boss in widest area (if any) +if features['wide_areas']: + boss_pos = features['wide_areas'][0] + boss = mcrfpy.Entity(boss_pos, texture, sprite_index=80) + grid.entities.append(boss) + +grid.center_camera(spawn) diff --git a/tests/snippets/171_procgen_wfc.py b/tests/snippets/171_procgen_wfc.py new file mode 100644 index 0000000..7de4b8c --- /dev/null +++ b/tests/snippets/171_procgen_wfc.py @@ -0,0 +1,140 @@ +# mcrf: objects=[Grid,Scene] verified=0.2.8-dev status=ok +import mcrfpy +import random + + +class WFC: + """Simple Wave Function Collapse for tile grids.""" + + def __init__(self, grid, rules): + self.grid = grid + self.rules = rules + self.width, self.height = int(grid.grid_size[0]), int(grid.grid_size[1]) + self.tiles = set(rules.keys()) + + self.possibilities = {} + for y in range(self.height): + for x in range(self.width): + self.possibilities[(x, y)] = set(self.tiles) + + def collapse(self): + """Run WFC until complete or contradiction.""" + while True: + cell = self._lowest_entropy_cell() + if cell is None: + return True # All cells collapsed + + x, y = cell + options = self.possibilities[(x, y)] + + if not options: + return False # Contradiction! + + chosen = random.choice(list(options)) + self.possibilities[(x, y)] = {chosen} + + self.grid.at((x, y)).tilesprite = chosen + + if not self._propagate(x, y): + return False + + return True + + def _lowest_entropy_cell(self): + best = None + best_entropy = float('inf') + + for pos, options in self.possibilities.items(): + if len(options) > 1: + entropy = len(options) + random.random() * 0.1 + if entropy < best_entropy: + best_entropy = entropy + best = pos + + return best + + def _propagate(self, start_x, start_y): + stack = [(start_x, start_y)] + + directions = { + 'N': (0, -1), 'S': (0, 1), + 'E': (1, 0), 'W': (-1, 0) + } + + while stack: + x, y = stack.pop() + current_options = self.possibilities[(x, y)] + + for dir_name, (dx, dy) in directions.items(): + nx, ny = x + dx, y + dy + + if not (0 <= nx < self.width and 0 <= ny < self.height): + continue + + neighbor_options = self.possibilities[(nx, ny)] + if len(neighbor_options) == 1: + continue + + allowed = set() + for tile in current_options: + if dir_name in self.rules[tile]: + allowed.update(self.rules[tile][dir_name]) + + new_options = neighbor_options & allowed + if new_options != neighbor_options: + if not new_options: + return False + + self.possibilities[(nx, ny)] = new_options + stack.append((nx, ny)) + + return True + + +# Setup +scene = mcrfpy.Scene("wfc_demo") +scene.activate() +mcrfpy.step(0.1) + +texture = mcrfpy.default_texture +grid = mcrfpy.Grid(grid_size=(30, 20), texture=texture, + pos=(0, 0), size=(480, 320)) +scene.children.append(grid) + +# Simple terrain rules +GRASS = 0 +DIRT = 1 +WATER = 2 +SAND = 3 # Transition between grass/water + +terrain_rules = { + GRASS: {'N': [GRASS, DIRT, SAND], 'S': [GRASS, DIRT, SAND], + 'E': [GRASS, DIRT, SAND], 'W': [GRASS, DIRT, SAND]}, + DIRT: {'N': [GRASS, DIRT], 'S': [GRASS, DIRT], + 'E': [GRASS, DIRT], 'W': [GRASS, DIRT]}, + WATER: {'N': [WATER, SAND], 'S': [WATER, SAND], + 'E': [WATER, SAND], 'W': [WATER, SAND]}, + SAND: {'N': [GRASS, WATER, SAND], 'S': [GRASS, WATER, SAND], + 'E': [GRASS, WATER, SAND], 'W': [GRASS, WATER, SAND]} +} + +# Seed with water in center +wfc = WFC(grid, terrain_rules) +for x in range(12, 18): + for y in range(8, 12): + wfc.possibilities[(x, y)] = {WATER} + grid.at((x, y)).tilesprite = WATER + wfc._propagate(x, y) + +# Generate rest +if wfc.collapse(): + print("Terrain generated successfully!") +else: + print("Generation failed") + +# Set walkability based on tiles +width, height = int(grid.grid_size[0]), int(grid.grid_size[1]) +for y in range(height): + for x in range(width): + cell = grid.at((x, y)) + cell.walkable = cell.tilesprite != WATER diff --git a/tests/snippets/172_rendering_and_visuals_1.py b/tests/snippets/172_rendering_and_visuals_1.py new file mode 100644 index 0000000..d552f73 --- /dev/null +++ b/tests/snippets/172_rendering_and_visuals_1.py @@ -0,0 +1,25 @@ +# mcrf: objects=[Caption,Color,Frame,Scene,Sprite] verified=0.2.8-dev status=ok +import mcrfpy + +# Create scene +scene = mcrfpy.Scene("game") +ui = scene.children + +# Rectangle/Frame +frame = mcrfpy.Frame(pos=(100, 100), size=(200, 150), + fill_color=mcrfpy.Color(50, 50, 50)) +frame.outline_color = mcrfpy.Color(255, 255, 255) +frame.outline = 2 +ui.append(frame) + +# Text +label = mcrfpy.Caption(text="Hello World!", pos=(10, 10)) +label.font_size = 24 +label.fill_color = mcrfpy.Color(255, 255, 255) +ui.append(label) + +# Sprite +sprite = mcrfpy.Sprite(x=50, y=50, sprite_index=0) +ui.append(sprite) + +mcrfpy.current_scene = scene diff --git a/tests/snippets/173_rendering_and_visuals_2.py b/tests/snippets/173_rendering_and_visuals_2.py new file mode 100644 index 0000000..1ca8137 --- /dev/null +++ b/tests/snippets/173_rendering_and_visuals_2.py @@ -0,0 +1,25 @@ +# mcrf: objects=[Color,Frame,Grid,Scene] verified=0.2.8-dev status=ok +import mcrfpy + +scene = mcrfpy.Scene("game") +ui = scene.children + +# Background layer (z=0) +background = mcrfpy.Frame(pos=(0, 0), size=(800, 600), + fill_color=mcrfpy.Color(20, 20, 20)) +background.z_index = 0 +ui.append(background) + +# Game layer (z=10) +grid = mcrfpy.Grid(grid_size=(50, 50), pos=(50, 50), size=(700, 500)) +grid.z_index = 10 +ui.append(grid) + +# UI layer (z=100) +hud = mcrfpy.Frame(pos=(0, 0), size=(800, 50), + fill_color=mcrfpy.Color(30, 30, 30, 200)) +hud.z_index = 100 +ui.append(hud) + +# Renders: background -> grid -> hud +mcrfpy.current_scene = scene diff --git a/tests/snippets/174_ui_widget_patterns_1.py b/tests/snippets/174_ui_widget_patterns_1.py new file mode 100644 index 0000000..423b58d --- /dev/null +++ b/tests/snippets/174_ui_widget_patterns_1.py @@ -0,0 +1,18 @@ +# mcrf: objects=[Color,Frame,Scene] verified=0.2.8-dev status=ok +# Setup Template - the basic scaffold shared by all widget patterns on this page. +import mcrfpy + +# Create scene and get UI collection +scene = mcrfpy.Scene("menu") +ui = scene.children + +# Create root container for the menu/HUD +root = mcrfpy.Frame(pos=(50, 50), size=(300, 400), + fill_color=mcrfpy.Color(30, 30, 40)) +root.outline_color = mcrfpy.Color(80, 80, 100) +root.outline = 2 +ui.append(root) + +# Add widgets to root.children... + +mcrfpy.current_scene = scene diff --git a/tests/snippets/175_ui_widget_patterns_2.py b/tests/snippets/175_ui_widget_patterns_2.py new file mode 100644 index 0000000..2b3197c --- /dev/null +++ b/tests/snippets/175_ui_widget_patterns_2.py @@ -0,0 +1,55 @@ +# mcrf: objects=[Caption,Color,Frame,InputState,MouseButton,Scene] verified=0.2.8-dev status=ok +# Button pattern - a clickable frame with label and hover feedback. +import mcrfpy + +scene = mcrfpy.Scene("menu") +ui = scene.children + +root = mcrfpy.Frame(pos=(50, 50), size=(300, 400), + fill_color=mcrfpy.Color(30, 30, 40)) +root.outline_color = mcrfpy.Color(80, 80, 100) +root.outline = 2 +ui.append(root) + + +def make_button(parent, pos, text, on_click): + """Create a button with hover effects.""" + btn = mcrfpy.Frame(pos=pos, size=(120, 32), + fill_color=mcrfpy.Color(60, 60, 80)) + btn.outline_color = mcrfpy.Color(100, 100, 140) + btn.outline = 1 + + label = mcrfpy.Caption(text=text, pos=(10, 6)) + label.fill_color = mcrfpy.Color(220, 220, 220) + btn.children.append(label) + + # Hover effects - on_enter/on_exit receive (pos: Vector) + btn.on_enter = lambda pos: setattr(btn, 'fill_color', + mcrfpy.Color(80, 80, 110)) + btn.on_exit = lambda pos: setattr(btn, 'fill_color', + mcrfpy.Color(60, 60, 80)) + + # Click handler receives (pos: Vector, button: MouseButton, action: InputState) + def handle_click(pos, button, action): + if button == mcrfpy.MouseButton.LEFT and action == mcrfpy.InputState.PRESSED: + on_click() + btn.on_click = handle_click + + parent.children.append(btn) + return btn + + +def start_new_game(): + print("Starting new game...") + + +def show_options(): + print("Showing options...") + + +# Usage +make_button(root, (20, 20), "New Game", lambda: start_new_game()) +make_button(root, (20, 60), "Options", lambda: show_options()) +make_button(root, (20, 100), "Quit", lambda: mcrfpy.current_scene.on_key) + +mcrfpy.current_scene = scene diff --git a/tests/snippets/176_ui_widget_patterns_3.py b/tests/snippets/176_ui_widget_patterns_3.py new file mode 100644 index 0000000..52a7b8e --- /dev/null +++ b/tests/snippets/176_ui_widget_patterns_3.py @@ -0,0 +1,61 @@ +# mcrf: objects=[Caption,Color,Frame,InputState,MouseButton,Scene] verified=0.2.8-dev status=ok +# Toggle / Checkbox pattern - a button that toggles state and updates its appearance. +import mcrfpy + +scene = mcrfpy.Scene("menu") +ui = scene.children + +root = mcrfpy.Frame(pos=(50, 50), size=(300, 400), + fill_color=mcrfpy.Color(30, 30, 40)) +root.outline_color = mcrfpy.Color(80, 80, 100) +root.outline = 2 +ui.append(root) + + +def make_toggle(parent, pos, label_text, initial=False, on_change=None): + """Create a toggle with visual state indicator.""" + state = {"checked": initial} + + frame = mcrfpy.Frame(pos=pos, size=(160, 28), + fill_color=mcrfpy.Color(40, 40, 50)) + + # Checkbox indicator + indicator = mcrfpy.Frame(pos=(6, 6), size=(16, 16)) + indicator.outline = 1 + indicator.outline_color = mcrfpy.Color(120, 120, 140) + frame.children.append(indicator) + + # Label + label = mcrfpy.Caption(text=label_text, pos=(30, 5)) + label.fill_color = mcrfpy.Color(200, 200, 200) + frame.children.append(label) + + def update_visual(): + if state["checked"]: + indicator.fill_color = mcrfpy.Color(100, 180, 100) + else: + indicator.fill_color = mcrfpy.Color(50, 50, 60) + + def toggle(pos, button, action): + if button == mcrfpy.MouseButton.LEFT and action == mcrfpy.InputState.PRESSED: + state["checked"] = not state["checked"] + update_visual() + if on_change: + on_change(state["checked"]) + + frame.on_click = toggle + update_visual() + + parent.children.append(frame) + return state # Return state dict for external access + + +def set_music_volume(v): + print(f"music volume: {v}") + + +# Usage +music_toggle = make_toggle(root, (20, 20), "Music", initial=True, + on_change=lambda on: set_music_volume(1.0 if on else 0.0)) + +mcrfpy.current_scene = scene diff --git a/tests/snippets/177_ui_widget_patterns_4.py b/tests/snippets/177_ui_widget_patterns_4.py new file mode 100644 index 0000000..6b08da7 --- /dev/null +++ b/tests/snippets/177_ui_widget_patterns_4.py @@ -0,0 +1,86 @@ +# mcrf: objects=[Caption,Color,Frame,InputState,Key,Scene] verified=0.2.8-dev status=ok +# Vertical Menu pattern - a list of selectable options with keyboard navigation. +import mcrfpy + +scene = mcrfpy.Scene("menu") +ui = scene.children + +root = mcrfpy.Frame(pos=(50, 50), size=(300, 400), + fill_color=mcrfpy.Color(30, 30, 40)) +root.outline_color = mcrfpy.Color(80, 80, 100) +root.outline = 2 +ui.append(root) + + +class VerticalMenu: + def __init__(self, parent, pos, options, on_select): + """ + options: list of (label, value) tuples + on_select: callback(value) when option chosen + """ + self.options = options + self.on_select = on_select + self.selected = 0 + + self.frame = mcrfpy.Frame(pos=pos, + size=(180, len(options) * 28 + 8), + fill_color=mcrfpy.Color(35, 35, 45)) + parent.children.append(self.frame) + + self.items = [] + for i, (label, value) in enumerate(options): + item = mcrfpy.Caption(text=label, pos=(12, 4 + i * 28)) + item.fill_color = mcrfpy.Color(180, 180, 180) + self.frame.children.append(item) + self.items.append(item) + + self._update_highlight() + + def _update_highlight(self): + for i, item in enumerate(self.items): + if i == self.selected: + item.fill_color = mcrfpy.Color(255, 220, 100) + else: + item.fill_color = mcrfpy.Color(180, 180, 180) + + def move_up(self): + self.selected = (self.selected - 1) % len(self.options) + self._update_highlight() + + def move_down(self): + self.selected = (self.selected + 1) % len(self.options) + self._update_highlight() + + def confirm(self): + _, value = self.options[self.selected] + self.on_select(value) + + +def handle_menu_choice(value): + print(f"chose: {value}") + + +# Usage +menu = VerticalMenu(root, (20, 20), [ + ("Continue", "continue"), + ("New Game", "new"), + ("Options", "options"), + ("Quit", "quit") +], on_select=handle_menu_choice) + + +# Keyboard navigation via scene.on_key +def handle_key(key, action): + if action != mcrfpy.InputState.PRESSED: + return + if key == mcrfpy.Key.UP: + menu.move_up() + elif key == mcrfpy.Key.DOWN: + menu.move_down() + elif key == mcrfpy.Key.ENTER: + menu.confirm() + + +scene.on_key = handle_key + +mcrfpy.current_scene = scene diff --git a/tests/snippets/178_ui_widget_patterns_5.py b/tests/snippets/178_ui_widget_patterns_5.py new file mode 100644 index 0000000..864d63c --- /dev/null +++ b/tests/snippets/178_ui_widget_patterns_5.py @@ -0,0 +1,75 @@ +# mcrf: objects=[Caption,Color,Frame,InputState,Key,Scene] verified=0.2.8-dev status=ok +# Modal Dialog pattern - a dialog that captures all input until dismissed. +import mcrfpy + +scene = mcrfpy.Scene("menu") +ui = scene.children + +root = mcrfpy.Frame(pos=(50, 50), size=(300, 400), + fill_color=mcrfpy.Color(30, 30, 40)) +root.outline_color = mcrfpy.Color(80, 80, 100) +root.outline = 2 +ui.append(root) + + +class ModalDialog: + def __init__(self, scene, message, on_dismiss=None): + self.scene = scene + self.on_dismiss = on_dismiss + ui = scene.children + + # Semi-transparent backdrop + self.backdrop = mcrfpy.Frame(pos=(0, 0), size=(1024, 768), + fill_color=mcrfpy.Color(0, 0, 0, 160)) + self.backdrop.z_index = 900 + self.backdrop.on_click = lambda pos, btn, action: None # Block clicks + ui.append(self.backdrop) + + # Dialog box + self.dialog = mcrfpy.Frame(pos=(312, 284), size=(400, 200), + fill_color=mcrfpy.Color(50, 50, 65)) + self.dialog.outline_color = mcrfpy.Color(120, 120, 150) + self.dialog.outline = 2 + self.dialog.z_index = 901 + ui.append(self.dialog) + + # Message + msg = mcrfpy.Caption(text=message, pos=(20, 20)) + msg.fill_color = mcrfpy.Color(220, 220, 220) + self.dialog.children.append(msg) + + # OK button + ok_btn = mcrfpy.Frame(pos=(150, 140), size=(100, 36), + fill_color=mcrfpy.Color(70, 100, 70)) + ok_btn.outline = 1 + ok_btn.outline_color = mcrfpy.Color(100, 150, 100) + ok_btn.on_click = lambda pos, btn, action: self.close() + self.dialog.children.append(ok_btn) + + ok_label = mcrfpy.Caption(text="OK", pos=(35, 8)) + ok_label.fill_color = mcrfpy.Color(220, 255, 220) + ok_btn.children.append(ok_label) + + # Capture keyboard + self._prev_handler = scene.on_key + + def modal_keys(key, action): + if action == mcrfpy.InputState.PRESSED: + if key == mcrfpy.Key.ENTER or key == mcrfpy.Key.ESCAPE: + self.close() + scene.on_key = modal_keys + + def close(self): + ui = self.scene.children + ui.remove(self.backdrop) + ui.remove(self.dialog) + if self._prev_handler: + self.scene.on_key = self._prev_handler + if self.on_dismiss: + self.on_dismiss() + + +# Usage +dialog = ModalDialog(scene, "Game saved successfully!") + +mcrfpy.current_scene = scene diff --git a/tests/snippets/179_ui_widget_patterns_6.py b/tests/snippets/179_ui_widget_patterns_6.py new file mode 100644 index 0000000..e79b9af --- /dev/null +++ b/tests/snippets/179_ui_widget_patterns_6.py @@ -0,0 +1,75 @@ +# mcrf: objects=[Caption,Color,Frame,InputState,Key,Scene] verified=0.2.8-dev status=ok +# Hotbar / Quick Slots pattern - number keys (1-9) mapped to inventory slots. +import mcrfpy + +scene = mcrfpy.Scene("menu") +ui = scene.children + +root = mcrfpy.Frame(pos=(50, 50), size=(300, 400), + fill_color=mcrfpy.Color(30, 30, 40)) +root.outline_color = mcrfpy.Color(80, 80, 100) +root.outline = 2 +ui.append(root) + + +class Hotbar: + def __init__(self, parent, pos, slot_count=9): + self.slots = [] + self.items = [None] * slot_count + self.selected = 0 + + self.frame = mcrfpy.Frame(pos=pos, + size=(slot_count * 36 + 8, 44), + fill_color=mcrfpy.Color(30, 30, 40, 200)) + parent.children.append(self.frame) + + for i in range(slot_count): + slot = mcrfpy.Frame(pos=(4 + i * 36, 4), size=(32, 32), + fill_color=mcrfpy.Color(50, 50, 60)) + slot.outline = 1 + slot.outline_color = mcrfpy.Color(80, 80, 100) + self.frame.children.append(slot) + self.slots.append(slot) + + num = mcrfpy.Caption(text=str((i + 1) % 10), pos=(2, 2)) + num.fill_color = mcrfpy.Color(100, 100, 120) + slot.children.append(num) + + self._update_selection() + + def _update_selection(self): + for i, slot in enumerate(self.slots): + if i == self.selected: + slot.outline_color = mcrfpy.Color(200, 180, 80) + slot.outline = 2 + else: + slot.outline_color = mcrfpy.Color(80, 80, 100) + slot.outline = 1 + + def select(self, index): + if 0 <= index < len(self.slots): + self.selected = index + self._update_selection() + + +# Usage +hotbar = Hotbar(root, (20, 340)) + +# Key mapping for number keys +num_keys = { + mcrfpy.Key.NUM_1: 0, mcrfpy.Key.NUM_2: 1, mcrfpy.Key.NUM_3: 2, + mcrfpy.Key.NUM_4: 3, mcrfpy.Key.NUM_5: 4, mcrfpy.Key.NUM_6: 5, + mcrfpy.Key.NUM_7: 6, mcrfpy.Key.NUM_8: 7, mcrfpy.Key.NUM_9: 8, +} + + +def handle_key(key, action): + if action != mcrfpy.InputState.PRESSED: + return + if key in num_keys: + hotbar.select(num_keys[key]) + + +scene.on_key = handle_key + +mcrfpy.current_scene = scene diff --git a/tests/snippets/180_ui_widget_patterns_7.py b/tests/snippets/180_ui_widget_patterns_7.py new file mode 100644 index 0000000..f4ebd8c --- /dev/null +++ b/tests/snippets/180_ui_widget_patterns_7.py @@ -0,0 +1,66 @@ +# mcrf: objects=[Caption,Color,Frame,InputState,MouseButton,Scene] verified=0.2.8-dev status=ok +# Draggable Window pattern - a frame that can be dragged by its title bar. +import mcrfpy + +scene = mcrfpy.Scene("menu") +ui = scene.children + +root = mcrfpy.Frame(pos=(50, 50), size=(300, 400), + fill_color=mcrfpy.Color(30, 30, 40)) +root.outline_color = mcrfpy.Color(80, 80, 100) +root.outline = 2 +ui.append(root) + + +class DraggableWindow: + def __init__(self, parent, pos, size, title): + self.dragging = False + self.drag_offset = (0, 0) + + self.frame = mcrfpy.Frame(pos=pos, size=size, + fill_color=mcrfpy.Color(45, 45, 55)) + self.frame.outline = 1 + self.frame.outline_color = mcrfpy.Color(100, 100, 120) + parent.children.append(self.frame) + + # Title bar + self.title_bar = mcrfpy.Frame(pos=(0, 0), size=(size[0], 24), + fill_color=mcrfpy.Color(60, 60, 80)) + self.frame.children.append(self.title_bar) + + title_label = mcrfpy.Caption(text=title, pos=(8, 4)) + title_label.fill_color = mcrfpy.Color(200, 200, 220) + self.title_bar.children.append(title_label) + + self.content_y = 28 + + # Drag handling + # on_click: (pos: Vector, button: MouseButton, action: InputState) + def start_drag(pos, button, action): + if button == mcrfpy.MouseButton.LEFT and action == mcrfpy.InputState.PRESSED: + self.dragging = True + self.drag_offset = (pos.x - self.frame.x, pos.y - self.frame.y) + + # on_move: (pos: Vector) + def on_move(pos): + if self.dragging: + self.frame.x = pos.x - self.drag_offset[0] + self.frame.y = pos.y - self.drag_offset[1] + + # on_exit: (pos: Vector) + def stop_drag(pos): + self.dragging = False + + self.title_bar.on_click = start_drag + self.title_bar.on_move = on_move + self.title_bar.on_exit = stop_drag + + +# Usage +window = DraggableWindow(root, (20, 20), (250, 300), "Inventory") + +# Add content to window.frame.children at y >= window.content_y +item_list = mcrfpy.Caption(text="Items here...", pos=(10, window.content_y + 10)) +window.frame.children.append(item_list) + +mcrfpy.current_scene = scene diff --git a/tests/snippets/181_ui_alignment_1.py b/tests/snippets/181_ui_alignment_1.py new file mode 100644 index 0000000..904aa53 --- /dev/null +++ b/tests/snippets/181_ui_alignment_1.py @@ -0,0 +1,18 @@ +# mcrf: objects=[Alignment,Color,Frame,Scene] verified=0.2.8-dev status=ok +import mcrfpy + +scene = mcrfpy.Scene("ui_alignment_quickstart") +mcrfpy.current_scene = scene + +# Create parent container +parent = mcrfpy.Frame(pos=(100, 100), size=(400, 300)) +scene.children.append(parent) + +# Create centered child +child = mcrfpy.Frame(pos=(0, 0), size=(100, 50)) +child.fill_color = mcrfpy.Color(200, 50, 50) +child.align = mcrfpy.Alignment.CENTER + +# Add to parent - alignment triggers automatically +parent.children.append(child) +# child is now at (150, 125) - centered in the 400x300 parent diff --git a/tests/snippets/182_ui_alignment_2.py b/tests/snippets/182_ui_alignment_2.py new file mode 100644 index 0000000..8e2a1ef --- /dev/null +++ b/tests/snippets/182_ui_alignment_2.py @@ -0,0 +1,24 @@ +# mcrf: objects=[Alignment,Frame,Scene] verified=0.2.8-dev status=ok +import mcrfpy + +scene = mcrfpy.Scene("ui_alignment_margins") +mcrfpy.current_scene = scene + +parent = mcrfpy.Frame(pos=(100, 100), size=(400, 300)) +scene.children.append(parent) + +# Health bar in top-left corner with 10px padding +health_bar = mcrfpy.Frame(pos=(0, 0), size=(200, 30)) +health_bar.align = mcrfpy.Alignment.TOP_LEFT +health_bar.margin = 10.0 # 10px from top and left edges + +parent.children.append(health_bar) +# health_bar is now at (10, 10) + +# Element with different horizontal/vertical margins +element = mcrfpy.Frame(pos=(0, 0), size=(100, 100)) +element.align = mcrfpy.Alignment.BOTTOM_RIGHT +element.horiz_margin = 20.0 # 20px from right edge +element.vert_margin = 10.0 # 10px from bottom edge + +parent.children.append(element) diff --git a/tests/snippets/183_ui_alignment_3.py b/tests/snippets/183_ui_alignment_3.py new file mode 100644 index 0000000..6298a61 --- /dev/null +++ b/tests/snippets/183_ui_alignment_3.py @@ -0,0 +1,20 @@ +# mcrf: objects=[Alignment,Frame,Scene] verified=0.2.8-dev status=ok +import mcrfpy + +scene = mcrfpy.Scene("ui_alignment_reactive") +mcrfpy.current_scene = scene + +parent = mcrfpy.Frame(pos=(0, 0), size=(200, 200)) +scene.children.append(parent) + +child = mcrfpy.Frame(pos=(0, 0), size=(50, 50)) +child.align = mcrfpy.Alignment.CENTER +parent.children.append(child) + +print(f"Initial: ({child.x}, {child.y})") # (75, 75) + +# Resize parent +parent.w = 400 +parent.h = 400 + +print(f"After resize: ({child.x}, {child.y})") # (175, 175) diff --git a/tests/snippets/184_ui_alignment_4.py b/tests/snippets/184_ui_alignment_4.py new file mode 100644 index 0000000..3ce75c0 --- /dev/null +++ b/tests/snippets/184_ui_alignment_4.py @@ -0,0 +1,20 @@ +# mcrf: objects=[Alignment,Frame,Scene] verified=0.2.8-dev status=ok +import mcrfpy + +scene = mcrfpy.Scene("ui_alignment_freeze") +mcrfpy.current_scene = scene + +parent = mcrfpy.Frame(pos=(0, 0), size=(200, 200)) +scene.children.append(parent) + +child = mcrfpy.Frame(pos=(0, 0), size=(50, 50)) + +# Center the element first +child.align = mcrfpy.Alignment.CENTER +parent.children.append(child) # Positions at center + +# Freeze position +child.align = None + +# Now parent resizing won't move the child +parent.w = 800 # child stays at its current position diff --git a/tests/snippets/185_ui_alignment_5.py b/tests/snippets/185_ui_alignment_5.py new file mode 100644 index 0000000..56b67e4 --- /dev/null +++ b/tests/snippets/185_ui_alignment_5.py @@ -0,0 +1,41 @@ +# mcrf: objects=[Alignment,Caption,Color,Frame,Scene] verified=0.2.8-dev status=ok +import mcrfpy + + +def create_hud(scene): + """Create HUD with elements in each corner.""" + # Full-screen container + hud = mcrfpy.Frame(pos=(0, 0), size=(800, 600)) + hud.fill_color = mcrfpy.Color(0, 0, 0, 0) # Transparent + scene.children.append(hud) + + # Health in top-left + health = mcrfpy.Frame(pos=(0, 0), size=(200, 40)) + health.align = mcrfpy.Alignment.TOP_LEFT + health.margin = 10.0 + hud.children.append(health) + + # Score in top-right + score = mcrfpy.Caption(text="Score: 0", pos=(0, 0)) + score.align = mcrfpy.Alignment.TOP_RIGHT + score.margin = 10.0 + hud.children.append(score) + + # Inventory in bottom-left + inventory = mcrfpy.Frame(pos=(0, 0), size=(150, 100)) + inventory.align = mcrfpy.Alignment.BOTTOM_LEFT + inventory.margin = 10.0 + hud.children.append(inventory) + + # Minimap in bottom-right + minimap = mcrfpy.Frame(pos=(0, 0), size=(120, 120)) + minimap.align = mcrfpy.Alignment.BOTTOM_RIGHT + minimap.margin = 10.0 + hud.children.append(minimap) + + return hud + + +scene = mcrfpy.Scene("ui_alignment_hud") +mcrfpy.current_scene = scene +create_hud(scene) diff --git a/tests/snippets/186_ui_alignment_6.py b/tests/snippets/186_ui_alignment_6.py new file mode 100644 index 0000000..d18305c --- /dev/null +++ b/tests/snippets/186_ui_alignment_6.py @@ -0,0 +1,36 @@ +# mcrf: objects=[Alignment,Caption,Color,Frame,Scene] verified=0.2.8-dev status=ok +import mcrfpy + + +def show_modal(scene, title, message): + """Display a centered modal dialog.""" + # Overlay background + overlay = mcrfpy.Frame(pos=(0, 0), size=(800, 600)) + overlay.fill_color = mcrfpy.Color(0, 0, 0, 128) + scene.children.append(overlay) + + # Dialog box - centered + dialog = mcrfpy.Frame(pos=(0, 0), size=(300, 200)) + dialog.fill_color = mcrfpy.Color(40, 40, 50) + dialog.outline = 2 + dialog.outline_color = mcrfpy.Color(100, 100, 120) + dialog.align = mcrfpy.Alignment.CENTER + overlay.children.append(dialog) + + # Title - centered at top of dialog + title_caption = mcrfpy.Caption(text=title, pos=(0, 0)) + title_caption.align = mcrfpy.Alignment.TOP_CENTER + title_caption.vert_margin = 20.0 + dialog.children.append(title_caption) + + # Message - centered + msg_caption = mcrfpy.Caption(text=message, pos=(0, 0)) + msg_caption.align = mcrfpy.Alignment.CENTER + dialog.children.append(msg_caption) + + return overlay + + +scene = mcrfpy.Scene("ui_alignment_modal") +mcrfpy.current_scene = scene +show_modal(scene, "Game Over", "You have died.") diff --git a/tests/snippets/187_ui_alignment_7.py b/tests/snippets/187_ui_alignment_7.py new file mode 100644 index 0000000..786b421 --- /dev/null +++ b/tests/snippets/187_ui_alignment_7.py @@ -0,0 +1,35 @@ +# mcrf: objects=[Alignment,Caption,Color,Frame,Scene] verified=0.2.8-dev status=ok +import mcrfpy + + +def create_button_bar(parent): + """Create a button bar at the bottom-center.""" + bar = mcrfpy.Frame(pos=(0, 0), size=(300, 50)) + bar.fill_color = mcrfpy.Color(30, 30, 40) + bar.align = mcrfpy.Alignment.BOTTOM_CENTER + bar.vert_margin = 20.0 + parent.children.append(bar) + + # Buttons spaced within the bar + button_width = 80 + spacing = 20 + + for i, label in enumerate(["Attack", "Defend", "Item"]): + btn = mcrfpy.Frame(pos=(spacing + i * (button_width + spacing), 10), + size=(button_width, 30)) + btn.fill_color = mcrfpy.Color(60, 60, 80) + + text = mcrfpy.Caption(text=label, pos=(0, 0)) + text.align = mcrfpy.Alignment.CENTER + btn.children.append(text) + + bar.children.append(btn) + + return bar + + +scene = mcrfpy.Scene("ui_alignment_buttonbar") +mcrfpy.current_scene = scene +parent = mcrfpy.Frame(pos=(0, 0), size=(400, 300)) +scene.children.append(parent) +create_button_bar(parent) diff --git a/tests/snippets/188_ui_health_bar_1.py b/tests/snippets/188_ui_health_bar_1.py new file mode 100644 index 0000000..7ca3126 --- /dev/null +++ b/tests/snippets/188_ui_health_bar_1.py @@ -0,0 +1,72 @@ +# mcrf: objects=[Color,Frame,Scene] verified=0.2.8-dev status=ok +import mcrfpy + +class HealthBar: + """A simple health bar with background and fill.""" + + def __init__(self, x, y, w, h, current, maximum): + """ + Create a health bar. + + Args: + x, y: Position on screen + w, h: Size of the bar + current: Current health value + maximum: Maximum health value + """ + self.x = x + self.y = y + self.w = w + self.h = h + self.current = current + self.maximum = maximum + + # Background (empty bar) + self.background = mcrfpy.Frame(pos=(x, y), size=(w, h)) + self.background.fill_color = mcrfpy.Color(40, 40, 40) + self.background.outline = 1 + self.background.outline_color = mcrfpy.Color(80, 80, 80) + + # Fill (current health) + self.fill = mcrfpy.Frame(pos=(x, y), size=(w, h)) + self.fill.fill_color = mcrfpy.Color(220, 50, 50) + self.fill.outline = 0 + + # Update fill width + self._update_fill() + + def _update_fill(self): + """Update the fill frame width based on current/max ratio.""" + ratio = max(0, min(1, self.current / self.maximum)) + self.fill.w = self.w * ratio + + def set_health(self, current, maximum=None): + """ + Update health values. + + Args: + current: New current health + maximum: New maximum health (optional) + """ + self.current = current + if maximum is not None: + self.maximum = maximum + self._update_fill() + + def add_to_scene(self, ui): + """Add both frames to scene UI (background first, then fill).""" + ui.append(self.background) + ui.append(self.fill) + + +# Usage Example +scene = mcrfpy.Scene("health_demo") + +# Create a health bar +hp_bar = HealthBar(50, 50, 200, 20, current=75, maximum=100) +hp_bar.add_to_scene(scene.children) + +# Update health +hp_bar.set_health(50) # Now at 50% + +scene.activate() diff --git a/tests/snippets/189_ui_health_bar_2.py b/tests/snippets/189_ui_health_bar_2.py new file mode 100644 index 0000000..66aa63f --- /dev/null +++ b/tests/snippets/189_ui_health_bar_2.py @@ -0,0 +1,112 @@ +# mcrf: objects=[Caption,Color,Frame,Scene] verified=0.2.8-dev status=ok +import mcrfpy + +class EnhancedHealthBar: + """Health bar with text display, color transitions, and animations.""" + + def __init__(self, x, y, w, h, current, maximum, show_text=True): + self.x = x + self.y = y + self.w = w + self.h = h + self.current = current + self.maximum = maximum + self.show_text = show_text + + # Color thresholds (ratio -> color) + self.colors = { + 0.6: mcrfpy.Color(50, 205, 50), # Green when > 60% + 0.3: mcrfpy.Color(255, 165, 0), # Orange when > 30% + 0.0: mcrfpy.Color(220, 20, 20), # Red when <= 30% + } + + # Background frame with dark fill + self.background = mcrfpy.Frame(pos=(x, y), size=(w, h)) + self.background.fill_color = mcrfpy.Color(30, 30, 30) + self.background.outline = 2 + self.background.outline_color = mcrfpy.Color(100, 100, 100) + + # Fill frame (nested inside background conceptually) + padding = 2 + self.fill = mcrfpy.Frame( + pos=(x + padding, y + padding), + size=(w - padding * 2, h - padding * 2) + ) + self.fill.outline = 0 + + # Text label + self.label = None + if show_text: + self.label = mcrfpy.Caption( + text="", + x=x + w / 2 - 20, + y=y + h / 2 - 8 + ) + self.label.fill_color = mcrfpy.Color(255, 255, 255) + self.label.outline = 1 + self.label.outline_color = mcrfpy.Color(0, 0, 0) + + self._update() + + def _get_color_for_ratio(self, ratio): + """Get the appropriate color based on health ratio.""" + for threshold, color in sorted(self.colors.items(), reverse=True): + if ratio > threshold: + return color + # Return the lowest threshold color if ratio is 0 or below + return self.colors[0.0] + + def _update(self): + """Update fill width, color, and text.""" + ratio = max(0, min(1, self.current / self.maximum)) + + # Update fill width (accounting for padding) + padding = 2 + self.fill.w = (self.w - padding * 2) * ratio + + # Update color based on ratio + self.fill.fill_color = self._get_color_for_ratio(ratio) + + # Update text + if self.label: + self.label.text = f"{int(self.current)}/{int(self.maximum)}" + # Center the text + text_width = len(self.label.text) * 8 # Approximate + self.label.x = self.x + (self.w - text_width) / 2 + + def set_health(self, current, maximum=None): + """Update health values.""" + self.current = max(0, current) + if maximum is not None: + self.maximum = maximum + self._update() + + def damage(self, amount): + """Apply damage (convenience method).""" + self.set_health(self.current - amount) + + def heal(self, amount): + """Apply healing (convenience method).""" + self.set_health(min(self.maximum, self.current + amount)) + + def add_to_scene(self, ui): + """Add all components to scene UI.""" + ui.append(self.background) + ui.append(self.fill) + if self.label: + ui.append(self.label) + + +# Usage +scene = mcrfpy.Scene("demo") + +# Create enhanced health bar +hp = EnhancedHealthBar(50, 50, 250, 25, current=100, maximum=100) +hp.add_to_scene(scene.children) + +# Simulate damage +hp.damage(30) # Now 70/100, shows green +hp.damage(25) # Now 45/100, shows orange +hp.damage(20) # Now 25/100, shows red + +scene.activate() diff --git a/tests/snippets/190_ui_health_bar_3.py b/tests/snippets/190_ui_health_bar_3.py new file mode 100644 index 0000000..42d04c0 --- /dev/null +++ b/tests/snippets/190_ui_health_bar_3.py @@ -0,0 +1,100 @@ +# mcrf: objects=[Caption,Color,Frame,Scene] verified=0.2.8-dev status=ok +import mcrfpy + +class ResourceBar: + """Generic resource bar that can represent any stat.""" + + def __init__(self, x, y, w, h, current, maximum, + fill_color, bg_color=None, label=""): + self.x = x + self.y = y + self.w = w + self.h = h + self.current = current + self.maximum = maximum + self.label_text = label + + if bg_color is None: + bg_color = mcrfpy.Color(30, 30, 30) + + # Background + self.background = mcrfpy.Frame(pos=(x, y), size=(w, h)) + self.background.fill_color = bg_color + self.background.outline = 1 + self.background.outline_color = mcrfpy.Color(60, 60, 60) + + # Fill + self.fill = mcrfpy.Frame(pos=(x + 1, y + 1), size=(w - 2, h - 2)) + self.fill.fill_color = fill_color + self.fill.outline = 0 + + # Label (left side) + self.label = mcrfpy.Caption(text=label, x=x - 30, y=y + 2) + self.label.fill_color = mcrfpy.Color(200, 200, 200) + + self._update() + + def _update(self): + ratio = max(0, min(1, self.current / self.maximum)) + self.fill.w = (self.w - 2) * ratio + + def set_value(self, current, maximum=None): + self.current = max(0, current) + if maximum: + self.maximum = maximum + self._update() + + def add_to_scene(self, ui): + if self.label_text: + ui.append(self.label) + ui.append(self.background) + ui.append(self.fill) + + +class PlayerStats: + """Collection of resource bars for a player.""" + + def __init__(self, x, y): + bar_width = 200 + bar_height = 18 + spacing = 25 + + self.hp = ResourceBar( + x, y, bar_width, bar_height, + current=100, maximum=100, + fill_color=mcrfpy.Color(220, 50, 50), + label="HP" + ) + + self.mp = ResourceBar( + x, y + spacing, bar_width, bar_height, + current=50, maximum=50, + fill_color=mcrfpy.Color(50, 100, 220), + label="MP" + ) + + self.stamina = ResourceBar( + x, y + spacing * 2, bar_width, bar_height, + current=80, maximum=80, + fill_color=mcrfpy.Color(50, 180, 50), + label="SP" + ) + + def add_to_scene(self, ui): + self.hp.add_to_scene(ui) + self.mp.add_to_scene(ui) + self.stamina.add_to_scene(ui) + + +# Usage +scene = mcrfpy.Scene("stats_demo") + +stats = PlayerStats(80, 20) +stats.add_to_scene(scene.children) + +# Update individual stats +stats.hp.set_value(75) +stats.mp.set_value(30) +stats.stamina.set_value(60) + +scene.activate() diff --git a/tests/snippets/191_ui_health_bar_4.py b/tests/snippets/191_ui_health_bar_4.py new file mode 100644 index 0000000..0a51059 --- /dev/null +++ b/tests/snippets/191_ui_health_bar_4.py @@ -0,0 +1,120 @@ +# mcrf: objects=[Color,Frame,Scene,Timer] verified=0.2.8-dev status=ok +import mcrfpy + +class AnimatedHealthBar: + """Health bar with smooth fill animation.""" + + def __init__(self, x, y, w, h, current, maximum): + self.x = x + self.y = y + self.w = w + self.h = h + self.current = current + self.display_current = current # What's visually shown + self.maximum = maximum + self.timer_name = f"hp_anim_{id(self)}" + + # Background + self.background = mcrfpy.Frame(pos=(x, y), size=(w, h)) + self.background.fill_color = mcrfpy.Color(40, 40, 40) + self.background.outline = 2 + self.background.outline_color = mcrfpy.Color(60, 60, 60) + + # Damage preview (shows recent damage in different color) + self.damage_fill = mcrfpy.Frame(pos=(x + 2, y + 2), size=(w - 4, h - 4)) + self.damage_fill.fill_color = mcrfpy.Color(180, 50, 50) + self.damage_fill.outline = 0 + + # Main fill + self.fill = mcrfpy.Frame(pos=(x + 2, y + 2), size=(w - 4, h - 4)) + self.fill.fill_color = mcrfpy.Color(50, 200, 50) + self.fill.outline = 0 + + self._update_display() + + def _update_display(self): + """Update the visual fill based on display_current.""" + ratio = max(0, min(1, self.display_current / self.maximum)) + self.fill.w = (self.w - 4) * ratio + + # Color based on ratio + if ratio > 0.6: + self.fill.fill_color = mcrfpy.Color(50, 200, 50) + elif ratio > 0.3: + self.fill.fill_color = mcrfpy.Color(230, 180, 30) + else: + self.fill.fill_color = mcrfpy.Color(200, 50, 50) + + def set_health(self, new_current, animate=True): + """ + Set health with optional animation. + + Args: + new_current: New health value + animate: Whether to animate the transition + """ + old_current = self.current + self.current = max(0, min(self.maximum, new_current)) + + if not animate: + self.display_current = self.current + self._update_display() + return + + # Show damage preview immediately + if self.current < old_current: + damage_ratio = self.current / self.maximum + self.damage_fill.w = (self.w - 4) * (old_current / self.maximum) + + # Animate the fill + self._start_animation() + + def _start_animation(self): + """Start animating toward target health.""" + self._animating = True + + def animate_step(timer, runtime): + if not self._animating: + return + # Lerp toward target + diff = self.current - self.display_current + if abs(diff) < 0.5: + self.display_current = self.current + self._animating = False + # Also update damage preview + self.damage_fill.w = self.fill.w + else: + # Move 10% of the way each frame + self.display_current += diff * 0.1 + # Continue animation + mcrfpy.Timer(self.timer_name, animate_step, 16) + + self._update_display() + + mcrfpy.Timer(self.timer_name, animate_step, 16) + + def damage(self, amount): + """Apply damage with animation.""" + self.set_health(self.current - amount, animate=True) + + def heal(self, amount): + """Apply healing with animation.""" + self.set_health(self.current + amount, animate=True) + + def add_to_scene(self, ui): + """Add all frames to scene.""" + ui.append(self.background) + ui.append(self.damage_fill) + ui.append(self.fill) + + +# Usage +scene = mcrfpy.Scene("animated_demo") + +hp_bar = AnimatedHealthBar(50, 50, 300, 30, current=100, maximum=100) +hp_bar.add_to_scene(scene.children) + +# Damage will animate smoothly +hp_bar.damage(40) + +scene.activate() diff --git a/tests/snippets/192_ui_menu_1.py b/tests/snippets/192_ui_menu_1.py new file mode 100644 index 0000000..1273efa --- /dev/null +++ b/tests/snippets/192_ui_menu_1.py @@ -0,0 +1,153 @@ +# mcrf: objects=[Caption,Color,Frame,InputState,Key,Scene] verified=0.2.8-dev status=ok +import mcrfpy + +class Menu: + """A keyboard-navigable menu widget.""" + + def __init__(self, x, y, options, on_select, title=None): + """ + Create a menu. + + Args: + x, y: Position on screen + options: List of option strings + on_select: Callback function(index, option_text) + title: Optional title string + """ + self.x = x + self.y = y + self.options = options + self.on_select = on_select + self.selected_index = 0 + + # Style settings + self.padding = 15 + self.line_height = 30 + self.width = 250 + + # Colors + self.color_normal = mcrfpy.Color(200, 200, 200) + self.color_selected = mcrfpy.Color(255, 255, 100) + self.color_bg = mcrfpy.Color(30, 30, 50, 230) + + # Calculate frame height + title_offset = self.line_height if title else 0 + height = self.padding * 2 + len(options) * self.line_height + title_offset + + # Create container frame + self.frame = mcrfpy.Frame(pos=(x, y), size=(self.width, height)) + self.frame.fill_color = self.color_bg + self.frame.outline = 2 + self.frame.outline_color = mcrfpy.Color(100, 100, 150) + + # Create title if provided + self.title_caption = None + if title: + self.title_caption = mcrfpy.Caption( + text=title, + x=x + self.padding, + y=y + self.padding + ) + self.title_caption.fill_color = mcrfpy.Color(255, 255, 255) + + # Create option captions + self.option_captions = [] + for i, option in enumerate(options): + caption_y = y + self.padding + title_offset + i * self.line_height + caption = mcrfpy.Caption( + text=option, + x=x + self.padding, + y=caption_y + ) + caption.fill_color = self.color_normal + self.option_captions.append(caption) + + # Update visual selection + self._update_selection() + + def _update_selection(self): + """Update visual highlight for selected option.""" + for i, caption in enumerate(self.option_captions): + if i == self.selected_index: + caption.fill_color = self.color_selected + caption.text = "> " + self.options[i] + else: + caption.fill_color = self.color_normal + caption.text = " " + self.options[i] + + def move_up(self): + """Move selection up (with wraparound).""" + self.selected_index = (self.selected_index - 1) % len(self.options) + self._update_selection() + + def move_down(self): + """Move selection down (with wraparound).""" + self.selected_index = (self.selected_index + 1) % len(self.options) + self._update_selection() + + def select(self): + """Trigger the callback for the current selection.""" + if self.on_select: + self.on_select(self.selected_index, self.options[self.selected_index]) + + def add_to_scene(self, ui): + """Add menu to scene UI.""" + ui.append(self.frame) + if self.title_caption: + ui.append(self.title_caption) + for caption in self.option_captions: + ui.append(caption) + + def handle_key(self, key): + """ + Handle keyboard input. + + Args: + key: Key name string + + Returns: + True if key was handled, False otherwise + """ + if key == mcrfpy.Key.UP or key == mcrfpy.Key.W: + self.move_up() + return True + elif key == mcrfpy.Key.DOWN or key == mcrfpy.Key.S: + self.move_down() + return True + elif key == mcrfpy.Key.ENTER or key == mcrfpy.Key.SPACE: + self.select() + return True + return False + + +# Usage Example +scene = mcrfpy.Scene("menu_demo") + +# Create other scenes for navigation +game_scene = mcrfpy.Scene("game") +options_scene = mcrfpy.Scene("options") + +def on_menu_select(index, option): + print(f"Selected: {option} (index {index})") + if option == "Start Game": + game_scene.activate() + elif option == "Options": + options_scene.activate() + elif option == "Quit": + mcrfpy.exit() + +menu = Menu( + 300, 200, + ["Start Game", "Options", "Quit"], + on_menu_select, + title="Main Menu" +) +menu.add_to_scene(scene.children) + +def handle_keys(key, state): + if state != mcrfpy.InputState.PRESSED: + return + menu.handle_key(key) + +scene.on_key = handle_keys +scene.activate() diff --git a/tests/snippets/193_ui_menu_2.py b/tests/snippets/193_ui_menu_2.py new file mode 100644 index 0000000..af90af6 --- /dev/null +++ b/tests/snippets/193_ui_menu_2.py @@ -0,0 +1,282 @@ +# mcrf: objects=[Caption,Color,Frame,InputState,Key,Scene] verified=0.2.8-dev status=ok +import mcrfpy + +class EnhancedMenu: + """Menu with descriptions, submenus, and visual polish.""" + + def __init__(self, x, y, options, title=None): + """ + Create an enhanced menu. + + Args: + x, y: Position + options: List of dicts with keys: + 'label': Display text + 'action': Callback function (optional) + 'description': Help text (optional) + 'submenu': EnhancedMenu instance (optional) + 'enabled': Boolean (optional, default True) + """ + self.x = x + self.y = y + self.options = options + self.title = title + self.selected_index = 0 + self.active = True + self.parent_menu = None + + # Layout + self.padding = 20 + self.line_height = 32 + self.width = 300 + + # Colors + self.colors = { + 'bg': mcrfpy.Color(25, 25, 40, 240), + 'border': mcrfpy.Color(80, 80, 120), + 'title': mcrfpy.Color(255, 220, 100), + 'normal': mcrfpy.Color(180, 180, 180), + 'selected': mcrfpy.Color(255, 255, 255), + 'disabled': mcrfpy.Color(80, 80, 80), + 'description': mcrfpy.Color(150, 150, 180), + 'highlight_bg': mcrfpy.Color(60, 60, 100), + } + + self._build_ui() + + def _build_ui(self): + """Construct UI elements.""" + title_offset = self.line_height + 10 if self.title else 0 + desc_height = 50 # Space for description + height = ( + self.padding * 2 + + len(self.options) * self.line_height + + title_offset + + desc_height + ) + + # Main frame + self.frame = mcrfpy.Frame(pos=(self.x, self.y), size=(self.width, height)) + self.frame.fill_color = self.colors['bg'] + self.frame.outline = 2 + self.frame.outline_color = self.colors['border'] + + # Title + self.title_caption = None + if self.title: + self.title_caption = mcrfpy.Caption( + text=self.title, + x=self.x + self.padding, + y=self.y + self.padding + ) + self.title_caption.fill_color = self.colors['title'] + + # Selection highlight frame + self.highlight = mcrfpy.Frame( + pos=(self.x + 5, self.y + self.padding + title_offset), + size=(self.width - 10, self.line_height) + ) + self.highlight.fill_color = self.colors['highlight_bg'] + self.highlight.outline = 0 + + # Option labels + self.option_captions = [] + for i, opt in enumerate(self.options): + caption_y = self.y + self.padding + title_offset + i * self.line_height + 5 + caption = mcrfpy.Caption( + text=opt.get('label', '???'), + x=self.x + self.padding + 10, + y=caption_y + ) + enabled = opt.get('enabled', True) + caption.fill_color = self.colors['normal'] if enabled else self.colors['disabled'] + self.option_captions.append(caption) + + # Add arrow for submenus + if opt.get('submenu'): + arrow = mcrfpy.Caption( + text=">", + x=self.x + self.width - self.padding - 15, + y=caption_y + ) + arrow.fill_color = caption.fill_color + opt['_arrow'] = arrow + + # Description text at bottom + desc_y = self.y + height - desc_height + 5 + self.description = mcrfpy.Caption( + text="", + x=self.x + self.padding, + y=desc_y + ) + self.description.fill_color = self.colors['description'] + + self._update_selection() + + def _update_selection(self): + """Update visuals for current selection.""" + title_offset = self.line_height + 10 if self.title else 0 + + # Move highlight + self.highlight.y = self.y + self.padding + title_offset + self.selected_index * self.line_height + + # Update caption colors + for i, caption in enumerate(self.option_captions): + opt = self.options[i] + enabled = opt.get('enabled', True) + + if i == self.selected_index: + caption.fill_color = self.colors['selected'] if enabled else self.colors['disabled'] + else: + caption.fill_color = self.colors['normal'] if enabled else self.colors['disabled'] + + # Update description + desc = self.options[self.selected_index].get('description', '') + self.description.text = desc + + def move_up(self): + """Move selection up, skipping disabled options.""" + if not self.active: + return + + start = self.selected_index + while True: + self.selected_index = (self.selected_index - 1) % len(self.options) + if self.options[self.selected_index].get('enabled', True): + break + if self.selected_index == start: + break # All disabled, stay put + self._update_selection() + + def move_down(self): + """Move selection down, skipping disabled options.""" + if not self.active: + return + + start = self.selected_index + while True: + self.selected_index = (self.selected_index + 1) % len(self.options) + if self.options[self.selected_index].get('enabled', True): + break + if self.selected_index == start: + break + self._update_selection() + + def select(self): + """Execute the selected option.""" + if not self.active: + return None + + opt = self.options[self.selected_index] + + if not opt.get('enabled', True): + return None + + if opt.get('submenu'): + return opt['submenu'] + + if opt.get('action'): + opt['action']() + + return None + + def back(self): + """Return to parent menu.""" + return self.parent_menu + + def add_to_scene(self, ui): + """Add all elements to scene.""" + ui.append(self.frame) + ui.append(self.highlight) + if self.title_caption: + ui.append(self.title_caption) + for i, caption in enumerate(self.option_captions): + ui.append(caption) + if '_arrow' in self.options[i]: + ui.append(self.options[i]['_arrow']) + ui.append(self.description) + + def remove_from_scene(self, ui): + """Remove all elements from scene.""" + elements = [self.frame, self.highlight, self.description] + if self.title_caption: + elements.append(self.title_caption) + elements.extend(self.option_captions) + for i, opt in enumerate(self.options): + if '_arrow' in opt: + elements.append(opt['_arrow']) + + for elem in elements: + try: + ui.remove(elem) + except: + pass + + +# Usage with submenus +scene = mcrfpy.Scene("menu") + +# Create options submenu +options_menu = EnhancedMenu(350, 220, [ + {'label': 'Sound Volume', 'description': 'Adjust sound effects volume'}, + {'label': 'Music Volume', 'description': 'Adjust background music volume'}, + {'label': 'Fullscreen', 'description': 'Toggle fullscreen mode'}, + {'label': 'Back', 'action': lambda: switch_menu(main_menu)}, +]) +options_menu.title = "Options" + +# Create main menu +main_menu = EnhancedMenu(300, 200, [ + { + 'label': 'New Game', + 'description': 'Start a new adventure', + 'action': lambda: print("Starting new game...") + }, + { + 'label': 'Continue', + 'description': 'Load your saved game', + 'enabled': False, # Disabled if no save exists + }, + { + 'label': 'Options', + 'description': 'Configure game settings', + 'submenu': options_menu + }, + { + 'label': 'Quit', + 'description': 'Exit the game', + 'action': lambda: mcrfpy.exit() + }, +]) +main_menu.title = "My Game" + +options_menu.parent_menu = main_menu +current_menu = main_menu +main_menu.add_to_scene(scene.children) + +def switch_menu(new_menu): + global current_menu + current_menu.remove_from_scene(scene.children) + current_menu = new_menu + current_menu.add_to_scene(scene.children) + +def handle_keys(key, state): + global current_menu + if state != mcrfpy.InputState.PRESSED: + return + + if key == mcrfpy.Key.UP or key == mcrfpy.Key.W: + current_menu.move_up() + elif key == mcrfpy.Key.DOWN or key == mcrfpy.Key.S: + current_menu.move_down() + elif key == mcrfpy.Key.ENTER or key == mcrfpy.Key.SPACE: + result = current_menu.select() + if result: # Submenu returned + switch_menu(result) + elif key == mcrfpy.Key.ESCAPE: + parent = current_menu.back() + if parent: + switch_menu(parent) + +scene.on_key = handle_keys +scene.activate() diff --git a/tests/snippets/194_ui_menu_3.py b/tests/snippets/194_ui_menu_3.py new file mode 100644 index 0000000..2673eba --- /dev/null +++ b/tests/snippets/194_ui_menu_3.py @@ -0,0 +1,162 @@ +# mcrf: objects=[Caption,Color,Frame,InputState,Key,Scene] verified=0.2.8-dev status=ok +import mcrfpy +class Menu: + """A keyboard-navigable menu widget.""" + + def __init__(self, x, y, options, on_select, title=None): + """ + Create a menu. + + Args: + x, y: Position on screen + options: List of option strings + on_select: Callback function(index, option_text) + title: Optional title string + """ + self.x = x + self.y = y + self.options = options + self.on_select = on_select + self.selected_index = 0 + + # Style settings + self.padding = 15 + self.line_height = 30 + self.width = 250 + + # Colors + self.color_normal = mcrfpy.Color(200, 200, 200) + self.color_selected = mcrfpy.Color(255, 255, 100) + self.color_bg = mcrfpy.Color(30, 30, 50, 230) + + # Calculate frame height + title_offset = self.line_height if title else 0 + height = self.padding * 2 + len(options) * self.line_height + title_offset + + # Create container frame + self.frame = mcrfpy.Frame(pos=(x, y), size=(self.width, height)) + self.frame.fill_color = self.color_bg + self.frame.outline = 2 + self.frame.outline_color = mcrfpy.Color(100, 100, 150) + + # Create title if provided + self.title_caption = None + if title: + self.title_caption = mcrfpy.Caption( + text=title, + x=x + self.padding, + y=y + self.padding + ) + self.title_caption.fill_color = mcrfpy.Color(255, 255, 255) + + # Create option captions + self.option_captions = [] + for i, option in enumerate(options): + caption_y = y + self.padding + title_offset + i * self.line_height + caption = mcrfpy.Caption( + text=option, + x=x + self.padding, + y=caption_y + ) + caption.fill_color = self.color_normal + self.option_captions.append(caption) + + # Update visual selection + self._update_selection() + + def _update_selection(self): + """Update visual highlight for selected option.""" + for i, caption in enumerate(self.option_captions): + if i == self.selected_index: + caption.fill_color = self.color_selected + caption.text = "> " + self.options[i] + else: + caption.fill_color = self.color_normal + caption.text = " " + self.options[i] + + def move_up(self): + """Move selection up (with wraparound).""" + self.selected_index = (self.selected_index - 1) % len(self.options) + self._update_selection() + + def move_down(self): + """Move selection down (with wraparound).""" + self.selected_index = (self.selected_index + 1) % len(self.options) + self._update_selection() + + def select(self): + """Trigger the callback for the current selection.""" + if self.on_select: + self.on_select(self.selected_index, self.options[self.selected_index]) + + def add_to_scene(self, ui): + """Add menu to scene UI.""" + ui.append(self.frame) + if self.title_caption: + ui.append(self.title_caption) + for caption in self.option_captions: + ui.append(caption) + + def handle_key(self, key): + """ + Handle keyboard input. + + Args: + key: Key name string + + Returns: + True if key was handled, False otherwise + """ + if key == mcrfpy.Key.UP or key == mcrfpy.Key.W: + self.move_up() + return True + elif key == mcrfpy.Key.DOWN or key == mcrfpy.Key.S: + self.move_down() + return True + elif key == mcrfpy.Key.ENTER or key == mcrfpy.Key.SPACE: + self.select() + return True + return False + + +# Setup +scene = mcrfpy.Scene("main_menu") + +# Background +bg = mcrfpy.Frame(pos=(0, 0), size=(1024, 768)) +bg.fill_color = mcrfpy.Color(20, 20, 35) +scene.children.append(bg) + +# Title +title = mcrfpy.Caption(text="DUNGEON QUEST", x=350, y=100) +title.fill_color = mcrfpy.Color(255, 200, 50) +scene.children.append(title) + +# Menu +def start_game(): + print("Starting game...") + +def show_options(): + print("Options...") + +menu = Menu( + 362, 250, + ["New Game", "Continue", "Options", "Quit"], + lambda i, opt: { + 0: start_game, + 1: lambda: print("Continue..."), + 2: show_options, + 3: mcrfpy.exit + }.get(i, lambda: None)(), + title="Main Menu" +) +menu.add_to_scene(scene.children) + +# Input +def on_key(key, state): + if state != mcrfpy.InputState.PRESSED: + return + menu.handle_key(key) + +scene.on_key = on_key +scene.activate() diff --git a/tests/snippets/195_ui_message_log_1.py b/tests/snippets/195_ui_message_log_1.py new file mode 100644 index 0000000..6a188b1 --- /dev/null +++ b/tests/snippets/195_ui_message_log_1.py @@ -0,0 +1,109 @@ +# mcrf: objects=[Caption,Color,Frame,Scene] verified=0.2.8-dev status=ok +import mcrfpy + +class MessageLog: + """A scrolling message log widget for displaying game events.""" + + def __init__(self, x, y, w, h, max_messages=100, line_height=18): + """ + Create a new message log. + + Args: + x, y: Position of the log on screen + w, h: Size of the log area + max_messages: Maximum messages to keep in history + line_height: Pixel height of each message line + """ + self.max_messages = max_messages + self.line_height = line_height + self.messages = [] # Store message data + + # Create the container frame + self.frame = mcrfpy.Frame(pos=(x, y), size=(w, h)) + self.frame.fill_color = mcrfpy.Color(20, 20, 30, 220) + self.frame.outline = 1 + self.frame.outline_color = mcrfpy.Color(60, 60, 80) + + # Calculate visible lines + self.visible_lines = int(h / line_height) + self.scroll_offset = 0 + + def add(self, text, color=None): + """ + Add a message to the log. + + Args: + text: The message text + color: Optional Color for the text (defaults to white) + """ + if color is None: + color = mcrfpy.Color(220, 220, 220) + + # Add to message buffer + self.messages.append({'text': text, 'color': color}) + + # Trim old messages if over limit + if len(self.messages) > self.max_messages: + self.messages = self.messages[-self.max_messages:] + + # Auto-scroll to bottom + self.scroll_offset = max(0, len(self.messages) - self.visible_lines) + + # Rebuild the display + self._rebuild_display() + + def _rebuild_display(self): + """Rebuild all visible Caption elements.""" + # Clear existing children (remove() takes a child object, not an index) + while len(self.frame.children) > 0: + self.frame.children.remove(self.frame.children[0]) + + # Calculate which messages to show + start_idx = self.scroll_offset + end_idx = min(start_idx + self.visible_lines, len(self.messages)) + + # Create Caption for each visible message + # (Frame.children are positioned in frame-local coordinates, + # not absolute screen coordinates) + for i, msg_idx in enumerate(range(start_idx, end_idx)): + msg = self.messages[msg_idx] + caption = mcrfpy.Caption( + text=msg['text'], + pos=(5, 5 + (i * self.line_height)) + ) + caption.fill_color = msg['color'] + self.frame.children.append(caption) + + def scroll_up(self, lines=1): + """Scroll the log up (show older messages).""" + self.scroll_offset = max(0, self.scroll_offset - lines) + self._rebuild_display() + + def scroll_down(self, lines=1): + """Scroll the log down (show newer messages).""" + max_offset = max(0, len(self.messages) - self.visible_lines) + self.scroll_offset = min(max_offset, self.scroll_offset + lines) + self._rebuild_display() + + def clear(self): + """Clear all messages from the log.""" + self.messages = [] + self.scroll_offset = 0 + self._rebuild_display() + + +# Usage Example +scene = mcrfpy.Scene("log_demo") + +# Create the message log +log = MessageLog(50, 400, 400, 200) +scene.children.append(log.frame) + +scene.activate() + +# Add some test messages +log.add("Welcome to the dungeon!") +log.add("You see a dark corridor ahead.", mcrfpy.Color(150, 150, 150)) +log.add("A goblin appears!", mcrfpy.Color(255, 100, 100)) +log.add("You attack the goblin for 5 damage.", mcrfpy.Color(255, 200, 100)) +log.add("The goblin strikes back!", mcrfpy.Color(255, 100, 100)) diff --git a/tests/snippets/196_ui_message_log_2.py b/tests/snippets/196_ui_message_log_2.py new file mode 100644 index 0000000..c8cb9e5 --- /dev/null +++ b/tests/snippets/196_ui_message_log_2.py @@ -0,0 +1,140 @@ +# mcrf: objects=[Caption,Color,Frame,Scene] verified=0.2.8-dev status=ok +import mcrfpy +import time + +class EnhancedMessageLog: + """Message log with categories, timestamps, and filtering.""" + + # Predefined message categories with colors + CATEGORIES = { + 'system': mcrfpy.Color(150, 150, 255), + 'combat': mcrfpy.Color(255, 100, 100), + 'loot': mcrfpy.Color(255, 215, 0), + 'dialog': mcrfpy.Color(100, 255, 100), + 'info': mcrfpy.Color(200, 200, 200), + } + + def __init__(self, x, y, w, h, max_messages=200, line_height=18): + self.max_messages = max_messages + self.line_height = line_height + self.messages = [] + self.filter_category = None # None = show all + + self.frame = mcrfpy.Frame(pos=(x, y), size=(w, h)) + self.frame.fill_color = mcrfpy.Color(15, 15, 25, 240) + self.frame.outline = 2 + self.frame.outline_color = mcrfpy.Color(80, 80, 120) + + self.visible_lines = int(h / line_height) + self.scroll_offset = 0 + + def add(self, text, category='info', show_time=False): + """ + Add a categorized message. + + Args: + text: Message text + category: Category key (system, combat, loot, dialog, info) + show_time: Whether to prepend timestamp + """ + color = self.CATEGORIES.get(category, self.CATEGORIES['info']) + + if show_time: + # Game time or real time - customize as needed + timestamp = time.strftime("%H:%M") + text = f"[{timestamp}] {text}" + + self.messages.append({ + 'text': text, + 'color': color, + 'category': category, + 'timestamp': time.time() + }) + + if len(self.messages) > self.max_messages: + self.messages = self.messages[-self.max_messages:] + + # Auto-scroll only if already at bottom + visible_msgs = self._get_filtered_messages() + if self.scroll_offset >= len(visible_msgs) - self.visible_lines - 1: + self.scroll_offset = max(0, len(visible_msgs) - self.visible_lines) + + self._rebuild_display() + + def _get_filtered_messages(self): + """Get messages matching current filter.""" + if self.filter_category is None: + return self.messages + return [m for m in self.messages if m['category'] == self.filter_category] + + def set_filter(self, category): + """ + Set category filter. + + Args: + category: Category to show, or None for all + """ + self.filter_category = category + self.scroll_offset = 0 + self._rebuild_display() + + def _rebuild_display(self): + """Rebuild visible messages.""" + while len(self.frame.children) > 0: + self.frame.children.remove(self.frame.children[0]) + + filtered = self._get_filtered_messages() + start_idx = self.scroll_offset + end_idx = min(start_idx + self.visible_lines, len(filtered)) + + # Positions are frame-local (relative to the frame's top-left corner) + for i, msg_idx in enumerate(range(start_idx, end_idx)): + msg = filtered[msg_idx] + caption = mcrfpy.Caption( + text=msg['text'], + pos=(8, 4 + (i * self.line_height)) + ) + caption.fill_color = msg['color'] + self.frame.children.append(caption) + + def scroll_up(self, lines=1): + self.scroll_offset = max(0, self.scroll_offset - lines) + self._rebuild_display() + + def scroll_down(self, lines=1): + filtered = self._get_filtered_messages() + max_offset = max(0, len(filtered) - self.visible_lines) + self.scroll_offset = min(max_offset, self.scroll_offset + lines) + self._rebuild_display() + + # Convenience methods for common message types + def system(self, text): + self.add(text, 'system') + + def combat(self, text): + self.add(text, 'combat') + + def loot(self, text): + self.add(text, 'loot') + + def dialog(self, text): + self.add(text, 'dialog') + + +# Usage +scene = mcrfpy.Scene("enhanced_log_demo") +log = EnhancedMessageLog(50, 400, 500, 250) +scene.children.append(log.frame) +scene.activate() + +log.system("Game loaded successfully.") +log.combat("You attack the skeleton!") +log.combat("The skeleton crumbles to dust.") +log.loot("You found 50 gold!") +log.dialog("The merchant says: 'Welcome, traveler!'") + +# Filter to only combat messages +log.set_filter('combat') + +# Show all again +log.set_filter(None) diff --git a/tests/snippets/197_ui_message_log_3.py b/tests/snippets/197_ui_message_log_3.py new file mode 100644 index 0000000..91e8dd3 --- /dev/null +++ b/tests/snippets/197_ui_message_log_3.py @@ -0,0 +1,145 @@ +# mcrf: objects=[Caption,Color,Frame,InputState,Key,Scene,Timer] verified=0.2.8-dev status=ok +import mcrfpy +import time + +class EnhancedMessageLog: + """Message log with categories, timestamps, and filtering.""" + + CATEGORIES = { + 'system': mcrfpy.Color(150, 150, 255), + 'combat': mcrfpy.Color(255, 100, 100), + 'loot': mcrfpy.Color(255, 215, 0), + 'dialog': mcrfpy.Color(100, 255, 100), + 'info': mcrfpy.Color(200, 200, 200), + } + + def __init__(self, x, y, w, h, max_messages=200, line_height=18): + self.max_messages = max_messages + self.line_height = line_height + self.messages = [] + self.filter_category = None + + self.frame = mcrfpy.Frame(pos=(x, y), size=(w, h)) + self.frame.fill_color = mcrfpy.Color(15, 15, 25, 240) + self.frame.outline = 2 + self.frame.outline_color = mcrfpy.Color(80, 80, 120) + + self.visible_lines = int(h / line_height) + self.scroll_offset = 0 + + def add(self, text, category='info', show_time=False): + color = self.CATEGORIES.get(category, self.CATEGORIES['info']) + + if show_time: + timestamp = time.strftime("%H:%M") + text = f"[{timestamp}] {text}" + + self.messages.append({ + 'text': text, + 'color': color, + 'category': category, + 'timestamp': time.time() + }) + + if len(self.messages) > self.max_messages: + self.messages = self.messages[-self.max_messages:] + + visible_msgs = self._get_filtered_messages() + if self.scroll_offset >= len(visible_msgs) - self.visible_lines - 1: + self.scroll_offset = max(0, len(visible_msgs) - self.visible_lines) + + self._rebuild_display() + + def _get_filtered_messages(self): + if self.filter_category is None: + return self.messages + return [m for m in self.messages if m['category'] == self.filter_category] + + def set_filter(self, category): + self.filter_category = category + self.scroll_offset = 0 + self._rebuild_display() + + def _rebuild_display(self): + while len(self.frame.children) > 0: + self.frame.children.remove(self.frame.children[0]) + + filtered = self._get_filtered_messages() + start_idx = self.scroll_offset + end_idx = min(start_idx + self.visible_lines, len(filtered)) + + for i, msg_idx in enumerate(range(start_idx, end_idx)): + msg = filtered[msg_idx] + caption = mcrfpy.Caption( + text=msg['text'], + pos=(8, 4 + (i * self.line_height)) + ) + caption.fill_color = msg['color'] + self.frame.children.append(caption) + + def scroll_up(self, lines=1): + self.scroll_offset = max(0, self.scroll_offset - lines) + self._rebuild_display() + + def scroll_down(self, lines=1): + filtered = self._get_filtered_messages() + max_offset = max(0, len(filtered) - self.visible_lines) + self.scroll_offset = min(max_offset, self.scroll_offset + lines) + self._rebuild_display() + + def system(self, text): + self.add(text, 'system') + + def combat(self, text): + self.add(text, 'combat') + + def loot(self, text): + self.add(text, 'loot') + + def dialog(self, text): + self.add(text, 'dialog') + + +# Initialize +scene = mcrfpy.Scene("game") + +# Create log at bottom of screen +log = EnhancedMessageLog(10, 500, 700, 250, line_height=20) +scene.children.append(log.frame) + +# Simulate game events +def simulate_combat(timer, runtime): + import random + events = [ + ("You swing your sword!", "combat"), + ("The orc dodges!", "combat"), + ("Critical hit!", "combat"), + ("You found a potion!", "loot"), + ] + event = random.choice(events) + log.add(event[0], event[1]) + +# Add messages every 2 seconds for demo +mcrfpy.Timer("combat_sim", simulate_combat, 2000) + +# Keyboard controls +def on_key(key, state): + if state != mcrfpy.InputState.PRESSED: + return + if key == mcrfpy.Key.PAGE_UP: + log.scroll_up(3) + elif key == mcrfpy.Key.PAGE_DOWN: + log.scroll_down(3) + elif key == mcrfpy.Key.C: + log.set_filter('combat') + elif key == mcrfpy.Key.L: + log.set_filter('loot') + elif key == mcrfpy.Key.A: + log.set_filter(None) # All + +scene.on_key = on_key + +log.system("Press PageUp/PageDown to scroll") +log.system("Press C for combat, L for loot, A for all") + +scene.activate() diff --git a/tests/snippets/198_ui_modal_dialog_1.py b/tests/snippets/198_ui_modal_dialog_1.py new file mode 100644 index 0000000..678aca3 --- /dev/null +++ b/tests/snippets/198_ui_modal_dialog_1.py @@ -0,0 +1,180 @@ +# mcrf: objects=[Caption,Color,Frame,InputState,Key,Scene] verified=0.2.8-dev status=ok +import mcrfpy + +class ModalDialog: + """A centered modal dialog with customizable buttons.""" + + def __init__(self, title, message, buttons, on_close=None): + """ + Create a modal dialog. + + Args: + title: Dialog title + message: Main message text + buttons: List of button labels (e.g., ["OK", "Cancel"]) + on_close: Callback function(button_index, button_label) + """ + self.title = title + self.message = message + self.buttons = buttons + self.on_close = on_close + self.visible = False + self.elements = [] + + # Calculate dimensions + self.dialog_width = 400 + self.dialog_height = 180 + self.button_width = 100 + self.button_height = 35 + + self._create_elements() + + def _create_elements(self): + """Create all UI elements for the dialog.""" + # Full-screen overlay + self.overlay = mcrfpy.Frame(pos=(0, 0), size=(1024, 768)) + self.overlay.fill_color = mcrfpy.Color(0, 0, 0, 150) + self.overlay.visible = False + + # Prevent clicks from passing through overlay + self.overlay.on_click = lambda pos, b, a: None + + # Center the dialog + dialog_x = (1024 - self.dialog_width) // 2 + dialog_y = (768 - self.dialog_height) // 2 + + # Dialog frame + self.dialog = mcrfpy.Frame( + pos=(dialog_x, dialog_y), + size=(self.dialog_width, self.dialog_height) + ) + self.dialog.fill_color = mcrfpy.Color(45, 45, 60) + self.dialog.outline = 2 + self.dialog.outline_color = mcrfpy.Color(100, 100, 130) + self.dialog.visible = False + + # Title + self.title_caption = mcrfpy.Caption( + text=self.title, + x=dialog_x + 20, + y=dialog_y + 15 + ) + self.title_caption.fill_color = mcrfpy.Color(255, 220, 100) + self.title_caption.visible = False + + # Message + self.message_caption = mcrfpy.Caption( + text=self.message, + x=dialog_x + 20, + y=dialog_y + 50 + ) + self.message_caption.fill_color = mcrfpy.Color(220, 220, 220) + self.message_caption.visible = False + + # Buttons + self.button_frames = [] + self.button_labels = [] + + total_button_width = len(self.buttons) * self.button_width + (len(self.buttons) - 1) * 15 + start_x = dialog_x + (self.dialog_width - total_button_width) // 2 + button_y = dialog_y + self.dialog_height - self.button_height - 20 + + for i, label in enumerate(self.buttons): + btn_x = start_x + i * (self.button_width + 15) + + btn_frame = mcrfpy.Frame(pos=(btn_x, button_y), size=(self.button_width, self.button_height)) + btn_frame.fill_color = mcrfpy.Color(70, 70, 90) + btn_frame.outline = 1 + btn_frame.outline_color = mcrfpy.Color(100, 100, 120) + btn_frame.visible = False + + # Click handler + def make_handler(index, text): + def handler(pos, button, action): + if action == mcrfpy.InputState.PRESSED: + self._on_button_click(index, text) + return handler + + btn_frame.on_click = make_handler(i, label) + self.button_frames.append(btn_frame) + + # Button label (centered) + label_x = btn_x + (self.button_width - len(label) * 8) // 2 + label_y = button_y + (self.button_height - 16) // 2 + + btn_label = mcrfpy.Caption(text=label, x=label_x, y=label_y) + btn_label.fill_color = mcrfpy.Color(255, 255, 255) + btn_label.visible = False + self.button_labels.append(btn_label) + + # Track all elements + self.elements = [ + self.overlay, self.dialog, + self.title_caption, self.message_caption + ] + self.elements.extend(self.button_frames) + self.elements.extend(self.button_labels) + + def _on_button_click(self, index, label): + """Handle button click.""" + self.hide() + if self.on_close: + self.on_close(index, label) + + def show(self): + """Show the dialog.""" + for elem in self.elements: + elem.visible = True + self.visible = True + + def hide(self): + """Hide the dialog.""" + for elem in self.elements: + elem.visible = False + self.visible = False + + def add_to_scene(self, ui): + """Add dialog to scene (call once during setup).""" + for elem in self.elements: + ui.append(elem) + + +# Usage Example +scene = mcrfpy.Scene("modal_demo") + +# Background content +bg = mcrfpy.Frame(pos=(0, 0), size=(1024, 768)) +bg.fill_color = mcrfpy.Color(30, 30, 45) +scene.children.append(bg) + +label = mcrfpy.Caption(text="Press SPACE to show dialog", x=350, y=350) +label.fill_color = mcrfpy.Color(200, 200, 200) +scene.children.append(label) + +# Create dialog +def on_dialog_close(index, label): + print(f"Dialog closed with: {label} (index {index})") + if label == "Yes": + print("User confirmed!") + else: + print("User cancelled.") + +dialog = ModalDialog( + "Confirm Action", + "Are you sure you want to proceed?", + ["Yes", "No"], + on_dialog_close +) +dialog.add_to_scene(scene.children) + +# Show dialog on keypress +def on_key(key, state): + if state != mcrfpy.InputState.PRESSED: + return + if key == mcrfpy.Key.SPACE and not dialog.visible: + dialog.show() + elif key == mcrfpy.Key.ESCAPE and dialog.visible: + dialog.hide() + +scene.on_key = on_key +scene.activate() diff --git a/tests/snippets/199_ui_modal_dialog_2.py b/tests/snippets/199_ui_modal_dialog_2.py new file mode 100644 index 0000000..418bba9 --- /dev/null +++ b/tests/snippets/199_ui_modal_dialog_2.py @@ -0,0 +1,332 @@ +# mcrf: objects=[Caption,Color,Frame,InputState,Key,Scene] verified=0.2.8-dev status=ok +import mcrfpy + +class DialogStyle: + """Predefined dialog styles.""" + INFO = { + 'bg': mcrfpy.Color(45, 55, 75), + 'border': mcrfpy.Color(80, 120, 180), + 'title': mcrfpy.Color(150, 200, 255), + 'btn_bg': mcrfpy.Color(60, 90, 130), + } + WARNING = { + 'bg': mcrfpy.Color(75, 65, 45), + 'border': mcrfpy.Color(180, 150, 80), + 'title': mcrfpy.Color(255, 220, 100), + 'btn_bg': mcrfpy.Color(130, 110, 60), + } + ERROR = { + 'bg': mcrfpy.Color(75, 45, 45), + 'border': mcrfpy.Color(180, 80, 80), + 'title': mcrfpy.Color(255, 150, 150), + 'btn_bg': mcrfpy.Color(130, 60, 60), + } + SUCCESS = { + 'bg': mcrfpy.Color(45, 75, 55), + 'border': mcrfpy.Color(80, 180, 100), + 'title': mcrfpy.Color(150, 255, 180), + 'btn_bg': mcrfpy.Color(60, 130, 80), + } + + +class EnhancedDialog: + """Modal dialog with styles, keyboard navigation, and animations.""" + + def __init__(self, title, message, buttons=None, style=None, on_close=None): + """ + Create an enhanced dialog. + + Args: + title: Dialog title + message: Main message (can include newlines) + buttons: Button labels (default ["OK"]) + style: DialogStyle dict (default INFO) + on_close: Callback(button_index, button_label) + """ + self.title = title + self.message = message + self.buttons = buttons or ["OK"] + self.style = style or DialogStyle.INFO + self.on_close = on_close + self.visible = False + self.selected_button = 0 + + self.dialog_width = 450 + self.dialog_height = 200 + + # Adjust height for multiline messages + lines = message.count('\n') + 1 + self.dialog_height = 150 + lines * 20 + + self._create_elements() + + def _create_elements(self): + """Build dialog UI.""" + # Overlay + self.overlay = mcrfpy.Frame(pos=(0, 0), size=(1024, 768)) + self.overlay.fill_color = mcrfpy.Color(0, 0, 0, 180) + self.overlay.visible = False + self.overlay.on_click = lambda pos, b, a: None + + # Dialog + dx = (1024 - self.dialog_width) // 2 + dy = (768 - self.dialog_height) // 2 + + self.dialog = mcrfpy.Frame(pos=(dx, dy), size=(self.dialog_width, self.dialog_height)) + self.dialog.fill_color = self.style['bg'] + self.dialog.outline = 3 + self.dialog.outline_color = self.style['border'] + self.dialog.visible = False + + # Title bar + self.title_bar = mcrfpy.Frame(pos=(dx + 2, dy + 2), size=(self.dialog_width - 4, 35)) + self.title_bar.fill_color = mcrfpy.Color( + self.style['border'].r, + self.style['border'].g, + self.style['border'].b, + 50 + ) + self.title_bar.outline = 0 + self.title_bar.visible = False + + # Title text + self.title_caption = mcrfpy.Caption( + text=self.title, + x=dx + 15, + y=dy + 10 + ) + self.title_caption.fill_color = self.style['title'] + self.title_caption.visible = False + + # Message + self.message_caption = mcrfpy.Caption( + text=self.message, + x=dx + 20, + y=dy + 50 + ) + self.message_caption.fill_color = mcrfpy.Color(230, 230, 230) + self.message_caption.visible = False + + # Buttons + self.button_frames = [] + self.button_labels = [] + + btn_width = 110 + btn_height = 38 + total_width = len(self.buttons) * btn_width + (len(self.buttons) - 1) * 15 + start_x = dx + (self.dialog_width - total_width) // 2 + btn_y = dy + self.dialog_height - btn_height - 20 + + for i, label in enumerate(self.buttons): + bx = start_x + i * (btn_width + 15) + + btn = mcrfpy.Frame(pos=(bx, btn_y), size=(btn_width, btn_height)) + btn.fill_color = self.style['btn_bg'] + btn.outline = 2 + btn.outline_color = mcrfpy.Color(150, 150, 170) + btn.visible = False + + def make_handler(idx, txt): + def handler(pos, button, action): + if action == mcrfpy.InputState.PRESSED: + self._select_button(idx, txt) + return handler + + btn.on_click = make_handler(i, label) + self.button_frames.append(btn) + + # Center label + lx = bx + (btn_width - len(label) * 8) // 2 + ly = btn_y + (btn_height - 16) // 2 + + lbl = mcrfpy.Caption(text=label, x=lx, y=ly) + lbl.fill_color = mcrfpy.Color(255, 255, 255) + lbl.visible = False + self.button_labels.append(lbl) + + self._update_button_highlight() + + def _update_button_highlight(self): + """Update button visual states.""" + for i, btn in enumerate(self.button_frames): + if i == self.selected_button: + btn.outline = 2 + btn.outline_color = mcrfpy.Color(255, 255, 100) + btn.fill_color = mcrfpy.Color( + min(255, self.style['btn_bg'].r + 30), + min(255, self.style['btn_bg'].g + 30), + min(255, self.style['btn_bg'].b + 30) + ) + else: + btn.outline = 1 + btn.outline_color = mcrfpy.Color(100, 100, 120) + btn.fill_color = self.style['btn_bg'] + + def _select_button(self, index, label): + """Handle button selection.""" + self.hide() + if self.on_close: + self.on_close(index, label) + + def navigate_left(self): + """Move selection left.""" + if not self.visible: + return + self.selected_button = (self.selected_button - 1) % len(self.buttons) + self._update_button_highlight() + + def navigate_right(self): + """Move selection right.""" + if not self.visible: + return + self.selected_button = (self.selected_button + 1) % len(self.buttons) + self._update_button_highlight() + + def confirm(self): + """Activate selected button.""" + if not self.visible: + return + self._select_button(self.selected_button, self.buttons[self.selected_button]) + + def show(self): + """Show the dialog.""" + self.selected_button = 0 + self._update_button_highlight() + + elements = [ + self.overlay, self.dialog, self.title_bar, + self.title_caption, self.message_caption + ] + self.button_frames + self.button_labels + + for elem in elements: + elem.visible = True + + self.visible = True + + def hide(self): + """Hide the dialog.""" + elements = [ + self.overlay, self.dialog, self.title_bar, + self.title_caption, self.message_caption + ] + self.button_frames + self.button_labels + + for elem in elements: + elem.visible = False + + self.visible = False + + def add_to_scene(self, ui): + """Add to scene UI.""" + elements = [ + self.overlay, self.dialog, self.title_bar, + self.title_caption, self.message_caption + ] + self.button_frames + self.button_labels + + for elem in elements: + ui.append(elem) + + def handle_key(self, key): + """Handle keyboard input. Returns True if handled.""" + if not self.visible: + return False + + if key == mcrfpy.Key.LEFT or key == mcrfpy.Key.A: + self.navigate_left() + return True + elif key == mcrfpy.Key.RIGHT or key == mcrfpy.Key.D: + self.navigate_right() + return True + elif key == mcrfpy.Key.ENTER or key == mcrfpy.Key.SPACE: + self.confirm() + return True + elif key == mcrfpy.Key.ESCAPE: + # Close with last button (usually Cancel) + self._select_button(len(self.buttons) - 1, self.buttons[-1]) + return True + + return False + + +# Usage with different styles +scene = mcrfpy.Scene("styled_dialogs") + +bg = mcrfpy.Frame(pos=(0, 0), size=(1024, 768)) +bg.fill_color = mcrfpy.Color(35, 35, 45) +scene.children.append(bg) + +# Current dialog reference +current_dialog = None + +def show_info(): + global current_dialog + current_dialog = EnhancedDialog( + "Information", + "This is an informational message.\nPress OK to continue.", + ["OK"], + DialogStyle.INFO + ) + current_dialog.add_to_scene(scene.children) + current_dialog.show() + +def show_warning(): + global current_dialog + current_dialog = EnhancedDialog( + "Warning", + "You are about to delete this item.\nThis action cannot be undone.", + ["Delete", "Cancel"], + DialogStyle.WARNING, + lambda i, l: print(f"Warning response: {l}") + ) + current_dialog.add_to_scene(scene.children) + current_dialog.show() + +def show_error(): + global current_dialog + current_dialog = EnhancedDialog( + "Error", + "Failed to save the file.\nPlease check disk space.", + ["Retry", "Cancel"], + DialogStyle.ERROR + ) + current_dialog.add_to_scene(scene.children) + current_dialog.show() + +def show_success(): + global current_dialog + current_dialog = EnhancedDialog( + "Success!", + "Your progress has been saved.", + ["Continue"], + DialogStyle.SUCCESS + ) + current_dialog.add_to_scene(scene.children) + current_dialog.show() + +# Help text +help_text = mcrfpy.Caption( + text="Press: 1=Info 2=Warning 3=Error 4=Success", + x=280, y=700 +) +help_text.fill_color = mcrfpy.Color(150, 150, 150) +scene.children.append(help_text) + +def on_key(key, state): + global current_dialog + if state != mcrfpy.InputState.PRESSED: + return + + # Let dialog handle input first + if current_dialog and current_dialog.handle_key(key): + return + + if key == mcrfpy.Key.NUM_1: + show_info() + elif key == mcrfpy.Key.NUM_2: + show_warning() + elif key == mcrfpy.Key.NUM_3: + show_error() + elif key == mcrfpy.Key.NUM_4: + show_success() + +scene.on_key = on_key +scene.activate() diff --git a/tests/snippets/200_ui_tooltip_1.py b/tests/snippets/200_ui_tooltip_1.py new file mode 100644 index 0000000..5e4645e --- /dev/null +++ b/tests/snippets/200_ui_tooltip_1.py @@ -0,0 +1,123 @@ +# mcrf: objects=[Caption,Color,Frame,Scene] verified=0.2.8-dev status=ok +import mcrfpy + +class Tooltip: + """A simple tooltip that follows the mouse.""" + + def __init__(self, text, width=200): + """ + Create a tooltip (initially hidden). + + Args: + text: Tooltip text + width: Maximum width + """ + self.text = text + self.visible = False + + # Tooltip frame + self.frame = mcrfpy.Frame(pos=(0, 0), size=(width, 30)) + self.frame.fill_color = mcrfpy.Color(40, 40, 50, 240) + self.frame.outline = 1 + self.frame.outline_color = mcrfpy.Color(100, 100, 120) + self.frame.visible = False + + # Tooltip text + self.caption = mcrfpy.Caption(text=text, pos=(5, 5)) + self.caption.fill_color = mcrfpy.Color(255, 255, 220) + self.caption.visible = False + + def show(self, x, y): + """Show tooltip at position.""" + # Offset from cursor + self.frame.x = x + 15 + self.frame.y = y + 15 + self.caption.x = self.frame.x + 8 + self.caption.y = self.frame.y + 6 + + # Keep on screen + if self.frame.x + self.frame.w > 1024: + self.frame.x = x - self.frame.w - 5 + self.caption.x = self.frame.x + 8 + if self.frame.y + self.frame.h > 768: + self.frame.y = y - self.frame.h - 5 + self.caption.y = self.frame.y + 6 + + self.frame.visible = True + self.caption.visible = True + self.visible = True + + def hide(self): + """Hide the tooltip.""" + self.frame.visible = False + self.caption.visible = False + self.visible = False + + def set_text(self, text): + """Update tooltip text.""" + self.text = text + self.caption.text = text + + def add_to_scene(self, ui): + """Add tooltip to scene (add last for top layer).""" + ui.append(self.frame) + ui.append(self.caption) + + +def make_hoverable(element, tooltip, tooltip_text=None): + """ + Make a UI element show a tooltip on hover. + + Args: + element: Frame, Caption, or Sprite to make hoverable + tooltip: Tooltip instance + tooltip_text: Text to show (optional, uses tooltip's default) + """ + def on_enter(pos): + if tooltip_text: + tooltip.set_text(tooltip_text) + tooltip.show(pos.x, pos.y) + + def on_move(pos): + tooltip.show(pos.x, pos.y) + + def on_exit(pos): + tooltip.hide() + + element.on_enter = on_enter + element.on_move = on_move + element.on_exit = on_exit + + +# Usage Example +scene = mcrfpy.Scene("tooltip_demo") +mcrfpy.current_scene = scene + +# Create some buttons +button1 = mcrfpy.Frame(pos=(100, 100), size=(120, 40)) +button1.fill_color = mcrfpy.Color(60, 60, 100) +button1.outline = 2 +button1.outline_color = mcrfpy.Color(100, 100, 150) +scene.children.append(button1) + +btn1_label = mcrfpy.Caption(text="Attack", pos=(125, 112)) +btn1_label.fill_color = mcrfpy.Color(255, 255, 255) +scene.children.append(btn1_label) + +button2 = mcrfpy.Frame(pos=(100, 160), size=(120, 40)) +button2.fill_color = mcrfpy.Color(60, 100, 60) +button2.outline = 2 +button2.outline_color = mcrfpy.Color(100, 150, 100) +scene.children.append(button2) + +btn2_label = mcrfpy.Caption(text="Defend", pos=(125, 172)) +btn2_label.fill_color = mcrfpy.Color(255, 255, 255) +scene.children.append(btn2_label) + +# Create shared tooltip (add last for top layer) +tooltip = Tooltip("", width=250) +tooltip.add_to_scene(scene.children) + +# Make buttons hoverable +make_hoverable(button1, tooltip, "Deal damage to the enemy") +make_hoverable(button2, tooltip, "Reduce incoming damage by 50%") diff --git a/tests/snippets/201_ui_tooltip_2.py b/tests/snippets/201_ui_tooltip_2.py new file mode 100644 index 0000000..dd96d95 --- /dev/null +++ b/tests/snippets/201_ui_tooltip_2.py @@ -0,0 +1,169 @@ +# mcrf: objects=[Caption,Color,Frame,Scene,Timer] verified=0.2.8-dev status=ok +import mcrfpy + +class TooltipManager: + """Manages tooltips for multiple UI elements.""" + + def __init__(self): + self.elements = {} # id(element) -> {text, title} mapping + self.tooltip_frame = None + self.tooltip_text = None + self.current_element = None + self.hover_delay = 500 # ms before showing tooltip + self.pending_element = None + self.pending_x = 0 + self.pending_y = 0 + + self._create_tooltip() + + def _create_tooltip(self): + """Create the tooltip UI elements.""" + self.tooltip_frame = mcrfpy.Frame(pos=(0, 0), size=(200, 60)) + self.tooltip_frame.fill_color = mcrfpy.Color(30, 30, 45, 245) + self.tooltip_frame.outline = 1 + self.tooltip_frame.outline_color = mcrfpy.Color(80, 80, 100) + self.tooltip_frame.visible = False + + self.tooltip_text = mcrfpy.Caption(text="", pos=(0, 0)) + self.tooltip_text.fill_color = mcrfpy.Color(255, 255, 230) + self.tooltip_text.visible = False + + def register(self, element, text, title=None): + """ + Register an element for tooltips. + + Args: + element: UI element to track + text: Tooltip description + title: Optional title (shown in different color) + """ + self.elements[id(element)] = { + 'element': element, + 'text': text, + 'title': title + } + + def on_enter(pos): + self._on_element_enter(element, pos.x, pos.y) + + def on_exit(pos): + self._on_element_exit(element) + + element.on_enter = on_enter + element.on_exit = on_exit + + def _on_element_enter(self, element, x, y): + """Handle mouse entering a registered element.""" + elem_id = id(element) + if elem_id not in self.elements: + return + + self.pending_element = element + self.pending_x = x + self.pending_y = y + + def show_after_delay(timer, runtime): + # Only show if the mouse is still over this element + if self.pending_element is element: + self._show_tooltip(element, self.pending_x, self.pending_y) + + timer_name = f"tooltip_delay_{id(self)}" + mcrfpy.Timer(timer_name, show_after_delay, self.hover_delay) + + def _on_element_exit(self, element): + """Handle mouse leaving a registered element.""" + if self.pending_element is element: + self.pending_element = None + if self.current_element is element: + self.hide() + + def _show_tooltip(self, element, x, y): + """Display tooltip for element.""" + elem_id = id(element) + if elem_id not in self.elements: + return + + data = self.elements[elem_id] + + # Build tooltip content + content = "" + if data.get('title'): + content = data['title'] + "\n" + content += data['text'] + + # Update tooltip text + self.tooltip_text.text = content + + # Calculate size based on content + lines = content.split('\n') + max_width = max(len(line) for line in lines) * 8 + 20 + height = len(lines) * 18 + 15 + + self.tooltip_frame.w = min(300, max(100, max_width)) + self.tooltip_frame.h = height + + # Position tooltip + self.tooltip_frame.x = x + 20 + self.tooltip_frame.y = y + 20 + + # Keep on screen + if self.tooltip_frame.x + self.tooltip_frame.w > 1000: + self.tooltip_frame.x = x - self.tooltip_frame.w - 10 + if self.tooltip_frame.y + self.tooltip_frame.h > 750: + self.tooltip_frame.y = y - self.tooltip_frame.h - 10 + + self.tooltip_text.x = self.tooltip_frame.x + 10 + self.tooltip_text.y = self.tooltip_frame.y + 8 + + # Show + self.tooltip_frame.visible = True + self.tooltip_text.visible = True + self.current_element = element + + def hide(self): + """Hide the tooltip.""" + self.tooltip_frame.visible = False + self.tooltip_text.visible = False + self.current_element = None + self.pending_element = None + + def add_to_scene(self, ui): + """Add tooltip to scene (call last for top layer).""" + ui.append(self.tooltip_frame) + ui.append(self.tooltip_text) + + +# Usage +scene = mcrfpy.Scene("demo") +mcrfpy.current_scene = scene + +# Create tooltip manager +tips = TooltipManager() + +# Create some items with tooltips +sword_frame = mcrfpy.Frame(pos=(100, 100), size=(64, 64)) +sword_frame.fill_color = mcrfpy.Color(80, 60, 40) +sword_frame.outline = 2 +sword_frame.outline_color = mcrfpy.Color(120, 100, 80) +scene.children.append(sword_frame) + +tips.register( + sword_frame, + "A rusty but reliable blade.\nDamage: 5-10\nSpeed: Fast", + title="Iron Sword" +) + +shield_frame = mcrfpy.Frame(pos=(180, 100), size=(64, 64)) +shield_frame.fill_color = mcrfpy.Color(60, 60, 80) +shield_frame.outline = 2 +shield_frame.outline_color = mcrfpy.Color(100, 100, 120) +scene.children.append(shield_frame) + +tips.register( + shield_frame, + "Blocks incoming attacks.\nDefense: +5\nWeight: Medium", + title="Steel Shield" +) + +# Add tooltip last for top layer +tips.add_to_scene(scene.children) diff --git a/tests/snippets/202_ui_tooltip_3.py b/tests/snippets/202_ui_tooltip_3.py new file mode 100644 index 0000000..c8e2507 --- /dev/null +++ b/tests/snippets/202_ui_tooltip_3.py @@ -0,0 +1,63 @@ +# mcrf: objects=[Caption,Color,Frame,Scene] verified=0.2.8-dev status=ok +import mcrfpy + +def create_info_icon(x, y, tooltip_text, ui): + """ + Create an info icon that shows tooltip on hover. + + Args: + x, y: Position of the icon + tooltip_text: Text to show + ui: Scene UI to add elements to + """ + # Info icon (small circle with "i") + icon = mcrfpy.Frame(pos=(x, y), size=(20, 20)) + icon.fill_color = mcrfpy.Color(70, 130, 180) + icon.outline = 1 + icon.outline_color = mcrfpy.Color(100, 160, 210) + + icon_label = mcrfpy.Caption(text="i", pos=(x + 6, y + 2)) + icon_label.fill_color = mcrfpy.Color(255, 255, 255) + + # Tooltip (positioned to the right of icon) + tip_frame = mcrfpy.Frame(pos=(x + 25, y - 5), size=(180, 50)) + tip_frame.fill_color = mcrfpy.Color(40, 40, 55, 240) + tip_frame.outline = 1 + tip_frame.outline_color = mcrfpy.Color(80, 80, 100) + tip_frame.visible = False + + tip_text = mcrfpy.Caption(text=tooltip_text, pos=(x + 33, y + 3)) + tip_text.fill_color = mcrfpy.Color(220, 220, 220) + tip_text.visible = False + + # Hover behavior — enter/exit fire directly, no polling needed + def on_icon_enter(pos): + tip_frame.visible = True + tip_text.visible = True + + def on_icon_exit(pos): + tip_frame.visible = False + tip_text.visible = False + + icon.on_enter = on_icon_enter + icon.on_exit = on_icon_exit + + # Add to scene + ui.append(icon) + ui.append(icon_label) + ui.append(tip_frame) + ui.append(tip_text) + + return icon + + +# Usage +scene = mcrfpy.Scene("info_demo") +mcrfpy.current_scene = scene + +# Setting with info icon +setting_label = mcrfpy.Caption(text="Difficulty:", pos=(100, 100)) +setting_label.fill_color = mcrfpy.Color(200, 200, 200) +scene.children.append(setting_label) + +create_info_icon(200, 98, "Affects enemy\nHP and damage", scene.children) diff --git a/tests/snippets/203_ui_tooltip_4.py b/tests/snippets/203_ui_tooltip_4.py new file mode 100644 index 0000000..3706700 --- /dev/null +++ b/tests/snippets/203_ui_tooltip_4.py @@ -0,0 +1,134 @@ +# mcrf: objects=[Caption,Color,Frame,Scene,Timer] verified=0.2.8-dev status=ok +import mcrfpy + +scene = mcrfpy.Scene("game") +mcrfpy.current_scene = scene + +# Background +bg = mcrfpy.Frame(pos=(0, 0), size=(1024, 768)) +bg.fill_color = mcrfpy.Color(25, 25, 35) +scene.children.append(bg) + +class TooltipManager: + """Manages tooltips for multiple UI elements.""" + + def __init__(self): + self.elements = {} + self.tooltip_frame = None + self.tooltip_text = None + self.current_element = None + self.hover_delay = 300 + self.pending_element = None + self.pending_x = 0 + self.pending_y = 0 + self._create_tooltip() + + def _create_tooltip(self): + self.tooltip_frame = mcrfpy.Frame(pos=(0, 0), size=(200, 60)) + self.tooltip_frame.fill_color = mcrfpy.Color(30, 30, 45, 245) + self.tooltip_frame.outline = 1 + self.tooltip_frame.outline_color = mcrfpy.Color(80, 80, 100) + self.tooltip_frame.visible = False + + self.tooltip_text = mcrfpy.Caption(text="", pos=(0, 0)) + self.tooltip_text.fill_color = mcrfpy.Color(255, 255, 230) + self.tooltip_text.visible = False + + def register(self, element, text, title=None): + self.elements[id(element)] = {'element': element, 'text': text, 'title': title} + + def on_enter(pos): + self.pending_element = element + self.pending_x, self.pending_y = pos.x, pos.y + + def show_after_delay(timer, runtime): + if self.pending_element is element: + self._show_tooltip(element, self.pending_x, self.pending_y) + + mcrfpy.Timer(f"tooltip_delay_{id(self)}", show_after_delay, self.hover_delay) + + def on_exit(pos): + if self.pending_element is element: + self.pending_element = None + if self.current_element is element: + self.hide() + + element.on_enter = on_enter + element.on_exit = on_exit + + def _show_tooltip(self, element, x, y): + elem_id = id(element) + if elem_id not in self.elements: + return + data = self.elements[elem_id] + + content = (data['title'] + "\n" if data.get('title') else "") + data['text'] + self.tooltip_text.text = content + + lines = content.split('\n') + max_width = max(len(line) for line in lines) * 8 + 20 + height = len(lines) * 18 + 15 + self.tooltip_frame.w = min(300, max(100, max_width)) + self.tooltip_frame.h = height + + self.tooltip_frame.x = x + 20 + self.tooltip_frame.y = y + 20 + if self.tooltip_frame.x + self.tooltip_frame.w > 1000: + self.tooltip_frame.x = x - self.tooltip_frame.w - 10 + if self.tooltip_frame.y + self.tooltip_frame.h > 750: + self.tooltip_frame.y = y - self.tooltip_frame.h - 10 + + self.tooltip_text.x = self.tooltip_frame.x + 10 + self.tooltip_text.y = self.tooltip_frame.y + 8 + + self.tooltip_frame.visible = True + self.tooltip_text.visible = True + self.current_element = element + + def hide(self): + self.tooltip_frame.visible = False + self.tooltip_text.visible = False + self.current_element = None + self.pending_element = None + + def add_to_scene(self, ui): + ui.append(self.tooltip_frame) + ui.append(self.tooltip_text) + + +# Create inventory slots with tooltips +class InventorySlot: + def __init__(self, x, y, item_name, item_desc, tooltip_mgr): + self.frame = mcrfpy.Frame(pos=(x, y), size=(50, 50)) + self.frame.fill_color = mcrfpy.Color(50, 50, 60) + self.frame.outline = 1 + self.frame.outline_color = mcrfpy.Color(80, 80, 90) + + self.label = mcrfpy.Caption(text=item_name[:3], pos=(x + 10, y + 15)) + self.label.fill_color = mcrfpy.Color(200, 200, 200) + + tooltip_mgr.register(self.frame, item_desc, title=item_name) + + def add_to_scene(self, ui): + ui.append(self.frame) + ui.append(self.label) + +# Setup tooltip manager +tips = TooltipManager() + +# Create inventory +items = [ + ("Health Potion", "Restores 50 HP\nConsumable"), + ("Mana Crystal", "Restores 30 MP\nConsumable"), + ("Iron Key", "Opens iron doors\nQuest Item"), + ("Gold Ring", "Worth 100 gold\nSell to merchant"), +] + +slots = [] +for i, (name, desc) in enumerate(items): + slot = InventorySlot(100 + i * 60, 100, name, desc, tips) + slot.add_to_scene(scene.children) + slots.append(slot) + +# Add tooltip last +tips.add_to_scene(scene.children) diff --git a/tests/snippets/204_fov.py b/tests/snippets/204_fov.py new file mode 100644 index 0000000..560e37d --- /dev/null +++ b/tests/snippets/204_fov.py @@ -0,0 +1,101 @@ +# mcrf: objects=[Color,ColorLayer,Entity,FOV,Grid,InputState,Key,Scene] verified=0.2.8-dev status=ok +import mcrfpy + +# Setup +scene = mcrfpy.Scene("game") +mcrfpy.current_scene = scene + +# Create grid +grid = mcrfpy.Grid( + grid_size=(40, 30), + texture=mcrfpy.default_texture, + pos=(0, 0), + size=(800, 600) +) +scene.children.append(grid) + +# Initialize map with walls and floors +for y in range(30): + for x in range(40): + point = grid.at(x, y) + # Border walls + if x == 0 or x == 39 or y == 0 or y == 29: + point.tilesprite = 1 + point.walkable = False + point.transparent = False + # Some interior walls + elif x == 15 and 5 <= y <= 20: + point.tilesprite = 1 + point.walkable = False + point.transparent = False + elif y == 15 and 20 <= x <= 30: + point.tilesprite = 1 + point.walkable = False + point.transparent = False + else: + point.tilesprite = 0 + point.walkable = True + point.transparent = True + +# Add FOV color layer +fov_layer = mcrfpy.ColorLayer(z_index=-1, name="fog") +grid.add_layer(fov_layer) + +# Track explored tiles +explored = [[False for _ in range(40)] for _ in range(30)] + +# Create player +player = mcrfpy.Entity( + grid_pos=(5, 5), + texture=mcrfpy.default_texture, + sprite_index=64 +) +grid.entities.append(player) + +# FOV update function +def update_fov(): + px, py = player.grid_x, player.grid_y + radius = 8 + + # Compute FOV + grid.compute_fov((px, py), radius=radius, algorithm=mcrfpy.FOV.SHADOW) + + # Update visibility display + for y in range(30): + for x in range(40): + if grid.is_in_fov(x, y): + explored[y][x] = True + fov_layer.set((x, y), mcrfpy.Color(0, 0, 0, 0)) + elif explored[y][x]: + fov_layer.set((x, y), mcrfpy.Color(30, 30, 50, 180)) + else: + fov_layer.set((x, y), mcrfpy.Color(0, 0, 0, 255)) + +# Initial FOV calculation +update_fov() +grid.center = player.pos + +# Movement with FOV update +def on_key(key, action): + if action != mcrfpy.InputState.PRESSED: + return + + dx, dy = 0, 0 + + if key in (mcrfpy.Key.UP, mcrfpy.Key.W): dy = -1 + elif key in (mcrfpy.Key.DOWN, mcrfpy.Key.S): dy = 1 + elif key in (mcrfpy.Key.LEFT, mcrfpy.Key.A): dx = -1 + elif key in (mcrfpy.Key.RIGHT, mcrfpy.Key.D): dx = 1 + else: + return + + new_x = player.grid_x + dx + new_y = player.grid_y + dy + + if 0 <= new_x < 40 and 0 <= new_y < 30: + if grid.at(new_x, new_y).walkable: + player.grid_pos = (new_x, new_y) + grid.center = player.pos + update_fov() + +scene.on_key = on_key diff --git a/tests/snippets/205_grid_system_1.py b/tests/snippets/205_grid_system_1.py new file mode 100644 index 0000000..67d0f18 --- /dev/null +++ b/tests/snippets/205_grid_system_1.py @@ -0,0 +1,17 @@ +# mcrf: objects=[Grid,Scene] verified=0.2.8-dev status=ok +import mcrfpy + +# Create a scene first +scene = mcrfpy.Scene("game") +mcrfpy.current_scene = scene + +# Basic grid creation +grid = mcrfpy.Grid( + grid_size=(50, 50), # 50x50 tiles + texture=mcrfpy.default_texture, # Built-in tileset + pos=(0, 0), # Screen position + size=(800, 600) # Display size in pixels +) + +# Add to scene +scene.children.append(grid) diff --git a/tests/snippets/206_grid_system_2.py b/tests/snippets/206_grid_system_2.py new file mode 100644 index 0000000..959eef9 --- /dev/null +++ b/tests/snippets/206_grid_system_2.py @@ -0,0 +1,94 @@ +# mcrf: objects=[Entity,Grid,InputState,Key,Scene] verified=0.2.8-dev status=ok +import mcrfpy +import random + +# Setup scene +scene = mcrfpy.Scene("game") +scene.activate() + +# Create grid +grid = mcrfpy.Grid( + grid_size=(50, 50), + texture=mcrfpy.default_texture, + pos=(0, 0), + size=(800, 600) +) +scene.children.append(grid) + +# Initialize map (all walls) +for y in range(50): + for x in range(50): + point = grid.at(x, y) + point.tilesprite = 1 # Wall + point.walkable = False + point.transparent = False + +# Carve rooms +rooms = [ + (5, 5, 10, 8), + (25, 5, 12, 10), + (8, 25, 15, 12), + (35, 30, 10, 15) +] + +for rx, ry, rw, rh in rooms: + for y in range(ry, ry + rh): + for x in range(rx, rx + rw): + point = grid.at(x, y) + point.tilesprite = 0 # Floor + point.walkable = True + point.transparent = True + +# Connect rooms with corridors +for i in range(len(rooms) - 1): + x1 = rooms[i][0] + rooms[i][2] // 2 + y1 = rooms[i][1] + rooms[i][3] // 2 + x2 = rooms[i+1][0] + rooms[i+1][2] // 2 + y2 = rooms[i+1][1] + rooms[i+1][3] // 2 + + # Horizontal then vertical + for x in range(min(x1, x2), max(x1, x2) + 1): + point = grid.at(x, y1) + point.tilesprite = 0 + point.walkable = True + point.transparent = True + + for y in range(min(y1, y2), max(y1, y2) + 1): + point = grid.at(x2, y) + point.tilesprite = 0 + point.walkable = True + point.transparent = True + +# Create player in first room +player = mcrfpy.Entity( + grid_pos=(10, 9), + texture=mcrfpy.default_texture, + sprite_index=64 +) +grid.entities.append(player) + +# Center camera on player +grid.center = player.pos + +# Movement handler -- on_key receives (Key, InputState) enums, not strings +def on_key(key, action): + if action != mcrfpy.InputState.PRESSED: + return + + dx, dy = 0, 0 + if key in (mcrfpy.Key.UP, mcrfpy.Key.W): dy = -1 + elif key in (mcrfpy.Key.DOWN, mcrfpy.Key.S): dy = 1 + elif key in (mcrfpy.Key.LEFT, mcrfpy.Key.A): dx = -1 + elif key in (mcrfpy.Key.RIGHT, mcrfpy.Key.D): dx = 1 + else: + return + + new_x = player.grid_x + dx + new_y = player.grid_y + dy + + # Check collision + if grid.at(new_x, new_y).walkable: + player.grid_pos = (new_x, new_y) + grid.center = player.pos # entity pixel position, matches grid.center's units + +scene.on_key = on_key diff --git a/tests/snippets/207_pathfinding.py b/tests/snippets/207_pathfinding.py new file mode 100644 index 0000000..ee1de54 --- /dev/null +++ b/tests/snippets/207_pathfinding.py @@ -0,0 +1,112 @@ +# mcrf: objects=[Entity,Grid,InputState,Key,Scene] verified=0.2.8-dev status=ok +import mcrfpy +import random + +# Setup +scene = mcrfpy.Scene("game") +mcrfpy.current_scene = scene + +# Create grid +grid = mcrfpy.Grid( + grid_size=(40, 30), + texture=mcrfpy.default_texture, + pos=(0, 0), + size=(800, 600) +) +scene.children.append(grid) + +# Initialize map +for y in range(30): + for x in range(40): + point = grid.at(x, y) + if x == 0 or x == 39 or y == 0 or y == 29: + point.tilesprite = 1 + point.walkable = False + else: + point.tilesprite = 0 + point.walkable = True + +# Add some obstacles +for _ in range(50): + x = random.randint(2, 37) + y = random.randint(2, 27) + point = grid.at(x, y) + point.tilesprite = 1 + point.walkable = False + +# Create player +player = mcrfpy.Entity(grid_pos=(5, 5), texture=mcrfpy.default_texture, sprite_index=64) +grid.entities.append(player) + +# Create enemies +enemies = [] +for _ in range(5): + while True: + x = random.randint(20, 35) + y = random.randint(5, 25) + if grid.at(x, y).walkable: + break + + enemy = mcrfpy.Entity(grid_pos=(x, y), texture=mcrfpy.default_texture, sprite_index=111) + grid.entities.append(enemy) + enemies.append(enemy) + +# Enemy AI +def update_enemies(): + """Move all enemies toward player.""" + # Use Dijkstra for efficiency with multiple enemies; cached by root position + dijkstra = grid.get_dijkstra_map(player.grid_pos) + + for enemy in enemies: + dist = dijkstra.distance(enemy.grid_pos) + + if dist is not None and dist <= 15: # Only chase if close enough + step = dijkstra.step_from(enemy.grid_pos) + + if step is not None: + next_x, next_y = int(step.x), int(step.y) + + # Don't walk into player (would be attack in real game) + if next_x == player.grid_x and next_y == player.grid_y: + continue + + # Don't walk into other enemies + blocked = False + for other in enemies: + if other != enemy: + if other.grid_x == next_x and other.grid_y == next_y: + blocked = True + break + + if not blocked: + enemy.grid_pos = (next_x, next_y) + +# Player movement +def on_key(key, action): + if action != mcrfpy.InputState.PRESSED: + return + + dx, dy = 0, 0 + + if key == mcrfpy.Key.UP or key == mcrfpy.Key.W: dy = -1 + elif key == mcrfpy.Key.DOWN or key == mcrfpy.Key.S: dy = 1 + elif key == mcrfpy.Key.LEFT or key == mcrfpy.Key.A: dx = -1 + elif key == mcrfpy.Key.RIGHT or key == mcrfpy.Key.D: dx = 1 + else: + return + + new_x = player.grid_x + dx + new_y = player.grid_y + dy + + if grid.at(new_x, new_y).walkable: + player.grid_pos = (new_x, new_y) + grid.center = player.pos + + # Enemy turn after player moves. get_dijkstra_map() is cached by root + # position, so a moved root simply produces a fresh map on next call. + update_enemies() + +scene.on_key = on_key + +# Initialize camera +grid.center = player.pos diff --git a/tests/snippets/208_scenes_1.py b/tests/snippets/208_scenes_1.py new file mode 100644 index 0000000..51af2f4 --- /dev/null +++ b/tests/snippets/208_scenes_1.py @@ -0,0 +1,20 @@ +# mcrf: objects=[Frame,InputState,Scene] verified=0.2.8-dev status=ok +import mcrfpy + +# Create scene with object reference +scene = mcrfpy.Scene("game") + +# Access children directly +scene.children.append(mcrfpy.Frame(pos=(0, 0), size=(800, 600))) + + +def my_handler(key, action): + if action == mcrfpy.InputState.PRESSED: + print(f"Key pressed: {key}") + + +# Set input handler on any scene (not just active) +scene.on_key = my_handler + +# Activate the scene +scene.activate() diff --git a/tests/snippets/209_scenes_2.py b/tests/snippets/209_scenes_2.py new file mode 100644 index 0000000..68be8ee --- /dev/null +++ b/tests/snippets/209_scenes_2.py @@ -0,0 +1,109 @@ +# mcrf: objects=[Caption,Color,Entity,Frame,Grid,InputState,Key,Scene] verified=0.2.8-dev status=ok +import mcrfpy + +class TitleScene(mcrfpy.Scene): + def __init__(self): + super().__init__("title") + + # Background + bg = mcrfpy.Frame(pos=(0, 0), size=(800, 600)) + bg.fill_color = mcrfpy.Color(20, 20, 40) + self.children.append(bg) + + # Title + title = mcrfpy.Caption(pos=(400, 150), text="DUNGEON EXPLORER") + title.fill_color = mcrfpy.Color(255, 215, 0) + self.children.append(title) + + # Subtitle + subtitle = mcrfpy.Caption(pos=(400, 220), text="A McRogueFace Demo") + subtitle.fill_color = mcrfpy.Color(180, 180, 180) + self.children.append(subtitle) + + # Instructions + start_text = mcrfpy.Caption(pos=(400, 400), text="Press SPACE to start") + start_text.fill_color = mcrfpy.Color(100, 255, 100) + self.children.append(start_text) + + self.on_key = self.handle_key + + def handle_key(self, key, action): + if action == mcrfpy.InputState.PRESSED and key == mcrfpy.Key.SPACE: + game.activate() + + +class GameScene(mcrfpy.Scene): + def __init__(self): + super().__init__("game") + self.grid = None + self.player = None + self.setup_game() + self.on_key = self.handle_key + + def setup_game(self): + # Create grid + self.grid = mcrfpy.Grid( + grid_size=(40, 30), + texture=mcrfpy.default_texture, + pos=(0, 0), + size=(800, 600) + ) + self.children.append(self.grid) + + # Initialize map + for y in range(30): + for x in range(40): + point = self.grid.at(x, y) + if x == 0 or x == 39 or y == 0 or y == 29: + point.tilesprite = 1 + point.walkable = False + else: + point.tilesprite = 0 + point.walkable = True + + # Create player + self.player = mcrfpy.Entity( + grid_pos=(20, 15), + texture=mcrfpy.default_texture, + sprite_index=64 + ) + self.grid.entities.append(self.player) + self.grid.center = self.player.pos + + def on_enter(self): + print("Game started!") + + def on_exit(self): + print("Game paused") + + def handle_key(self, key, action): + if action != mcrfpy.InputState.PRESSED: + return + + if key == mcrfpy.Key.ESCAPE: + title.activate() + return + + dx, dy = 0, 0 + if key in (mcrfpy.Key.UP, mcrfpy.Key.W): dy = -1 + elif key in (mcrfpy.Key.DOWN, mcrfpy.Key.S): dy = 1 + elif key in (mcrfpy.Key.LEFT, mcrfpy.Key.A): dx = -1 + elif key in (mcrfpy.Key.RIGHT, mcrfpy.Key.D): dx = 1 + + if dx or dy: + self.move_player(dx, dy) + + def move_player(self, dx, dy): + new_x = self.player.grid_x + dx + new_y = self.player.grid_y + dy + + if self.grid.at(new_x, new_y).walkable: + self.player.grid_pos = (new_x, new_y) + self.grid.center = self.player.pos + + +# Create and activate scenes +title = TitleScene() +game = GameScene() + +title.activate() diff --git a/tests/snippets/210_AStarPath.py b/tests/snippets/210_AStarPath.py new file mode 100644 index 0000000..7b777ea --- /dev/null +++ b/tests/snippets/210_AStarPath.py @@ -0,0 +1,34 @@ +# mcrf: objects=[Caption,Entity,Grid,Scene] verified=0.2.8-dev status=ok +import mcrfpy + +scene = mcrfpy.Scene("astar_demo") +mcrfpy.current_scene = scene + +grid = mcrfpy.Grid(grid_size=(10, 10)) +scene.children.append(grid) + +for y in range(10): + for x in range(10): + grid.at(x, y).walkable = True + +player = mcrfpy.Entity((0, 0), grid=grid) +target_x, target_y = 3, 3 + +# Create path from grid +path = grid.find_path(player.cell_pos, (target_x, target_y)) + +if path: + # Check path length + print(f"Path has {path.remaining} steps") + + # Peek at next step without consuming + next_pos = path.peek() + + # Walk the path step by step + while path: + next_step = path.walk() + x, y = next_step + player.cell_pos = (x, y) + +status = mcrfpy.Caption(text=f"Player at {player.cell_pos}", pos=(10, 10)) +scene.children.append(status) diff --git a/tests/snippets/211_Alignment.py b/tests/snippets/211_Alignment.py new file mode 100644 index 0000000..40b94f1 --- /dev/null +++ b/tests/snippets/211_Alignment.py @@ -0,0 +1,20 @@ +# mcrf: objects=[Alignment,Caption,Color,Frame,Scene,Sprite] verified=0.2.8-dev status=ok +import mcrfpy + +scene = mcrfpy.Scene("alignment_demo") +mcrfpy.current_scene = scene + +container = mcrfpy.Frame(pos=(50, 50), size=(400, 300), fill_color=mcrfpy.Color(40, 40, 60)) +scene.children.append(container) + +# Center a caption in its parent frame +caption = mcrfpy.Caption(text="Centered", pos=(0, 0)) +caption.align = mcrfpy.Alignment.CENTER +container.children.append(caption) + +# Position a sprite in the top-right corner with margin +sprite = mcrfpy.Sprite(texture=mcrfpy.default_texture, pos=(0, 0)) +sprite.align = mcrfpy.Alignment.TOP_RIGHT +sprite.horiz_margin = 10 +sprite.vert_margin = 10 +container.children.append(sprite) diff --git a/tests/snippets/212_Animation_1.py b/tests/snippets/212_Animation_1.py new file mode 100644 index 0000000..b030313 --- /dev/null +++ b/tests/snippets/212_Animation_1.py @@ -0,0 +1,22 @@ +# mcrf: objects=[Easing,Frame,Scene] verified=0.2.8-dev status=ok +import mcrfpy + +scene = mcrfpy.Scene("demo") +mcrfpy.current_scene = scene + +frame = mcrfpy.Frame(pos=(0, 0), size=(100, 100)) +scene.children.append(frame) + +# Animate a property: creates, starts, and registers the animation +frame.animate("x", 500.0, 2.0, mcrfpy.Easing.EASE_OUT_QUAD) + +# With a completion callback +def on_done(target, prop, value): + print(f"{prop} finished at {value}") + +frame.animate("opacity", 0.0, 1.0, callback=on_done) + +# The returned Animation handle can be inspected and controlled +anim = frame.animate("w", 300.0, 1.5) +print(anim.property, anim.duration, anim.is_complete) +anim.stop() # cancel without applying the final value diff --git a/tests/snippets/213_Animation_2.py b/tests/snippets/213_Animation_2.py new file mode 100644 index 0000000..bed98fc --- /dev/null +++ b/tests/snippets/213_Animation_2.py @@ -0,0 +1,16 @@ +# mcrf: objects=[Easing,Frame,Scene] verified=0.2.8-dev status=ok +import mcrfpy + +scene = mcrfpy.Scene("demo") +mcrfpy.current_scene = scene + +frame = mcrfpy.Frame(pos=(0, 0), size=(100, 100)) +scene.children.append(frame) + +def on_arrival(target, prop, value): + # target: the Frame that was animated + # prop: "x" + # value: 500.0 + print(f"{type(target).__name__}.{prop} reached {value}") + +frame.animate("x", 500.0, 2.0, mcrfpy.Easing.EASE_OUT_CUBIC, callback=on_arrival) diff --git a/tests/snippets/214_Animation_3.py b/tests/snippets/214_Animation_3.py new file mode 100644 index 0000000..1728a96 --- /dev/null +++ b/tests/snippets/214_Animation_3.py @@ -0,0 +1,23 @@ +# mcrf: objects=[Frame,Scene] verified=0.2.8-dev status=ok +import mcrfpy + +scene = mcrfpy.Scene("demo") +mcrfpy.current_scene = scene + +frame = mcrfpy.Frame(pos=(0, 0), size=(100, 100)) +scene.children.append(frame) + +frame.animate("x", 100.0, 1.0) + +# replace (default): the running animation jumps to its final value, +# then the new animation starts +frame.animate("x", 200.0, 1.0) + +# queue: run after the current animation on "x" finishes +frame.animate("x", 300.0, 1.0, conflict_mode="queue") + +# error: raise RuntimeError if "x" is already animating +try: + frame.animate("x", 400.0, 1.0, conflict_mode="error") +except RuntimeError as e: + print(e) diff --git a/tests/snippets/215_Animation_4.py b/tests/snippets/215_Animation_4.py new file mode 100644 index 0000000..6379b98 --- /dev/null +++ b/tests/snippets/215_Animation_4.py @@ -0,0 +1,19 @@ +# mcrf: objects=[Frame,Scene] verified=0.2.8-dev status=ok +import mcrfpy + +scene = mcrfpy.Scene("demo") +mcrfpy.current_scene = scene + +frame = mcrfpy.Frame(pos=(0, 0), size=(100, 100)) +scene.children.append(frame) + +anim = frame.animate("x", 500.0, 2.0) + +print(anim.property) # "x" +print(anim.duration) # 2.0 +print(anim.elapsed) # seconds since start +print(anim.is_complete) # False until finished (or complete() called) +print(anim.get_current_value()) # current interpolated value + +anim.complete() # jump to x=500 immediately; fires the callback if set +print(frame.x) # 500.0 diff --git a/tests/snippets/216_Animation_5.py b/tests/snippets/216_Animation_5.py new file mode 100644 index 0000000..f630366 --- /dev/null +++ b/tests/snippets/216_Animation_5.py @@ -0,0 +1,20 @@ +# mcrf: objects=[Frame,Scene] verified=0.2.8-dev status=ok +import mcrfpy + +scene = mcrfpy.Scene("demo") +mcrfpy.current_scene = scene + +frame = mcrfpy.Frame(pos=(0, 0), size=(50, 50)) +scene.children.append(frame) + +def step2(target, prop, value): + target.animate("y", 100.0, 0.3, callback=step3) + +def step3(target, prop, value): + target.animate("x", 0.0, 0.3, callback=step4) + +def step4(target, prop, value): + target.animate("y", 0.0, 0.3) + +# Trace a rectangle: each callback starts the next leg +frame.animate("x", 100.0, 0.3, callback=step2) diff --git a/tests/snippets/217_Animation_6.py b/tests/snippets/217_Animation_6.py new file mode 100644 index 0000000..2bebbbc --- /dev/null +++ b/tests/snippets/217_Animation_6.py @@ -0,0 +1,15 @@ +# mcrf: objects=[Scene,Sprite,Texture] verified=0.2.8-dev status=ok +import mcrfpy + +scene = mcrfpy.Scene("demo") +mcrfpy.current_scene = scene + +texture = mcrfpy.Texture("assets/kenney_tinydungeon.png", 16, 16) +sprite = mcrfpy.Sprite(texture=texture, sprite_index=84, pos=(100, 100)) +scene.children.append(sprite) + +# Cycle through frames 84-87 forever (0.6 seconds per cycle) +walk = sprite.animate("sprite_index", [84, 85, 86, 87], 0.6, loop=True) + +# Later, to stop the loop: +walk.stop() diff --git a/tests/snippets/218_Animation_7.py b/tests/snippets/218_Animation_7.py new file mode 100644 index 0000000..655bae2 --- /dev/null +++ b/tests/snippets/218_Animation_7.py @@ -0,0 +1,17 @@ +# mcrf: objects=[Easing,Grid,Scene,Texture] verified=0.2.8-dev status=ok +import mcrfpy + +scene = mcrfpy.Scene("demo") +mcrfpy.current_scene = scene + +texture = mcrfpy.Texture("assets/kenney_tinydungeon.png", 16, 16) +grid = mcrfpy.Grid(grid_size=(30, 20), pos=(0, 0), size=(480, 320), texture=texture) +scene.children.append(grid) + +# grid.center is in PIXELS. To pan to tile (14, 8) with 16x16 tiles, +# target the middle of that tile: +tile_x, tile_y = 14, 8 +target = (tile_x * 16 + 8, tile_y * 16 + 8) + +grid.animate("center", target, 0.4, mcrfpy.Easing.EASE_OUT_CUBIC) +grid.animate("zoom", 2.0, 0.4, mcrfpy.Easing.EASE_IN_OUT) diff --git a/tests/snippets/219_Animation_8.py b/tests/snippets/219_Animation_8.py new file mode 100644 index 0000000..f61d6d3 --- /dev/null +++ b/tests/snippets/219_Animation_8.py @@ -0,0 +1,16 @@ +# mcrf: objects=[Easing,Entity,Grid,Scene,Texture] verified=0.2.8-dev status=ok +import mcrfpy + +scene = mcrfpy.Scene("demo") +mcrfpy.current_scene = scene + +texture = mcrfpy.Texture("assets/kenney_tinydungeon.png", 16, 16) +grid = mcrfpy.Grid(grid_size=(10, 10), pos=(0, 0), size=(160, 160), texture=texture) +scene.children.append(grid) + +player = mcrfpy.Entity(grid_pos=(1, 1), texture=texture, sprite_index=84) +grid.entities.append(player) + +# Slide the drawn position across the grid in tile coordinates. +# cell_pos (the logical cell) is NOT changed by this animation. +player.animate("draw_x", 5.0, 0.5, mcrfpy.Easing.EASE_OUT_QUAD) diff --git a/tests/snippets/220_Animation_9.py b/tests/snippets/220_Animation_9.py new file mode 100644 index 0000000..f2e9e40 --- /dev/null +++ b/tests/snippets/220_Animation_9.py @@ -0,0 +1,17 @@ +# mcrf: objects=[Frame,Scene] verified=0.2.8-dev status=ok +import mcrfpy + +scene = mcrfpy.Scene("demo") +mcrfpy.current_scene = scene + +frame = mcrfpy.Frame(pos=(100, 100), size=(200, 80)) +scene.children.append(frame) + +# Animate a whole color (interpolates R, G, B, and A together) +frame.animate("fill_color", (255, 0, 0, 255), 1.0) + +# Animate a single channel +frame.animate("fill_color.a", 0.0, 0.5) + +# Delta mode: move 50 pixels down from wherever the frame is now +frame.animate("y", 50.0, 0.5, delta=True) diff --git a/tests/snippets/221_Animation_10.py b/tests/snippets/221_Animation_10.py new file mode 100644 index 0000000..4ecb474 --- /dev/null +++ b/tests/snippets/221_Animation_10.py @@ -0,0 +1,11 @@ +# mcrf: objects=[Caption,Scene] verified=0.2.8-dev status=ok +import mcrfpy + +scene = mcrfpy.Scene("demo") +mcrfpy.current_scene = scene + +caption = mcrfpy.Caption(text="", pos=(20, 20)) +scene.children.append(caption) + +# Typewriter effect: reveals the target string over 2 seconds +caption.animate("text", "You awaken in a dark cell.", 2.0) diff --git a/tests/snippets/222_Arc_1.py b/tests/snippets/222_Arc_1.py new file mode 100644 index 0000000..d99eb24 --- /dev/null +++ b/tests/snippets/222_Arc_1.py @@ -0,0 +1,26 @@ +# mcrf: objects=[Arc,Color,Scene] verified=0.2.8-dev status=ok +import mcrfpy + +scene = mcrfpy.Scene("arc_demo") +mcrfpy.current_scene = scene + +# Create a simple arc +arc = mcrfpy.Arc(center=(200, 200), radius=50, start_angle=0, end_angle=180) + +# Style it +arc.color = mcrfpy.Color(255, 0, 0) +arc.thickness = 3 + +# Add to scene +scene.children.append(arc) + +# Create a progress indicator (quarter circle) +progress = mcrfpy.Arc( + center=(100, 100), + radius=40, + start_angle=270, + end_angle=360, + color=mcrfpy.Color(0, 255, 0), + thickness=5 +) +scene.children.append(progress) diff --git a/tests/snippets/223_Arc_2.py b/tests/snippets/223_Arc_2.py new file mode 100644 index 0000000..d969eb7 --- /dev/null +++ b/tests/snippets/223_Arc_2.py @@ -0,0 +1,20 @@ +# mcrf: objects=[Arc,Color,Easing,Scene] verified=0.2.8-dev status=ok +import mcrfpy + +scene = mcrfpy.Scene("arc_spinner") +mcrfpy.current_scene = scene + +# Animated loading spinner +arc = mcrfpy.Arc( + center=(150, 150), + radius=30, + start_angle=0, + end_angle=270, + color=mcrfpy.Color(100, 150, 255), + thickness=4 +) +scene.children.append(arc) + +# Animate the arc rotating +arc.animate("start_angle", 360, 1.0, mcrfpy.Easing.LINEAR) +arc.animate("end_angle", 630, 1.0, mcrfpy.Easing.LINEAR) diff --git a/tests/snippets/224_BSP_1.py b/tests/snippets/224_BSP_1.py new file mode 100644 index 0000000..434cbf2 --- /dev/null +++ b/tests/snippets/224_BSP_1.py @@ -0,0 +1,42 @@ +# mcrf: objects=[BSP,Caption,Color,ColorLayer,Grid,Scene] verified=0.2.8-dev status=ok +# BSP Quick Reference - create a tree, split, and iterate leaves +import mcrfpy + +scene = mcrfpy.Scene("demo") +mcrfpy.current_scene = scene + +grid = mcrfpy.Grid( + grid_size=(80, 50), + texture=mcrfpy.default_texture, + pos=(0, 0), + size=(800, 500), +) +scene.children.append(grid) + +# Create a BSP tree covering a dungeon area +bsp = mcrfpy.BSP(pos=(0, 0), size=(80, 50)) + +# Split recursively into rooms +bsp.split_recursive(depth=4, min_size=(8, 8)) + +# Iterate over leaf nodes (rooms) +room_layer = mcrfpy.ColorLayer(z_index=1, name="rooms") +grid.add_layer(room_layer) + +for leaf in bsp: + x, y = leaf.pos + w, h = leaf.size + room_layer.set((x, y), mcrfpy.Color(200, 150, 100, 150)) + +# Use adjacency graph for corridor placement +for i, neighbors in enumerate(bsp.adjacency): + leaf = bsp.get_leaf(i) + for neighbor_idx in neighbors: + # leaf and bsp.get_leaf(neighbor_idx) share a wall + tiles = leaf.adjacent_tiles[neighbor_idx] + # tiles contains Vector coordinates for corridor placement + for tile in tiles: + pass # e.g. carve a corridor tile at (tile.x, tile.y) + +status = mcrfpy.Caption(text=f"BSP: {len(bsp)} rooms", pos=(10, 10)) +scene.children.append(status) diff --git a/tests/snippets/225_BSP_2.py b/tests/snippets/225_BSP_2.py new file mode 100644 index 0000000..e8881a2 --- /dev/null +++ b/tests/snippets/225_BSP_2.py @@ -0,0 +1,31 @@ +# mcrf: objects=[BSP,Caption,Scene,Traversal] verified=0.2.8-dev status=ok +# BSP traversal - iterate leaves and all nodes in various orders +import mcrfpy + +scene = mcrfpy.Scene("demo") +mcrfpy.current_scene = scene + +bsp = mcrfpy.BSP(pos=(0, 0), size=(80, 50)) +bsp.split_recursive(depth=4, min_size=(8, 8)) + +# len() returns number of leaf nodes +num_rooms = len(bsp) + +# Iterate directly over leaves +for leaf in bsp: + pass + +# Or use leaves() explicitly +for leaf in bsp.leaves(): + pass + +# Traverse all nodes (including internal) +lines = [] +for node in bsp.traverse(mcrfpy.Traversal.PRE_ORDER): + if node.is_leaf: + lines.append(f"Leaf: {node.pos}") + else: + lines.append(f"Split at: {node.split_position}") + +status = mcrfpy.Caption(text=f"BSP: {num_rooms} rooms, {len(lines)} nodes visited", pos=(10, 10)) +scene.children.append(status) diff --git a/tests/snippets/226_BSP_3.py b/tests/snippets/226_BSP_3.py new file mode 100644 index 0000000..0f135f5 --- /dev/null +++ b/tests/snippets/226_BSP_3.py @@ -0,0 +1,27 @@ +# mcrf: objects=[BSP,Caption,Scene] verified=0.2.8-dev status=ok +# BSP adjacency - connect rooms with corridors via the wall-tile map +import mcrfpy + +scene = mcrfpy.Scene("demo") +mcrfpy.current_scene = scene + +bsp = mcrfpy.BSP(pos=(0, 0), size=(80, 50)) +bsp.split_recursive(depth=4, min_size=(8, 8)) + +# Get all neighbor relationships +lines = [] +for i, neighbors in enumerate(bsp.adjacency): + lines.append(f"Leaf {i} is adjacent to: {neighbors}") + +# Get wall tiles between adjacent leaves +leaf = bsp.get_leaf(0) +wall_count = 0 +for neighbor_idx in leaf.adjacent_tiles.keys(): + tiles = leaf.adjacent_tiles[neighbor_idx] + # tiles is a tuple of Vector objects representing + # coordinates on THIS leaf's edge that border the neighbor + for tile in tiles: + wall_count += 1 + +status = mcrfpy.Caption(text=f"BSP: {len(lines)} leaves, {wall_count} shared wall tiles", pos=(10, 10)) +scene.children.append(status) diff --git a/tests/snippets/227_Caption.py b/tests/snippets/227_Caption.py new file mode 100644 index 0000000..821d9f3 --- /dev/null +++ b/tests/snippets/227_Caption.py @@ -0,0 +1,24 @@ +# mcrf: objects=[Caption,Color,Font,Scene] verified=0.2.8-dev status=ok +import mcrfpy + +scene = mcrfpy.Scene("caption_demo") +mcrfpy.current_scene = scene + +# Create a caption +title = mcrfpy.Caption(text="Hello, World!", pos=(100, 50), font=mcrfpy.Font("assets/JetbrainsMono.ttf")) + +# Style it +title.font_size = 24 +title.fill_color = mcrfpy.Color(255, 255, 255) +title.outline_color = mcrfpy.Color(0, 0, 0) +title.outline = 1 + +# Add to scene +scene.children.append(title) + +# Update text dynamically +score = 100 +title.text = f"Score: {score}" + +# Animate properties +title.animate("opacity", 0.0, 2.0, "easeOut") # Fade out diff --git a/tests/snippets/228_Circle_1.py b/tests/snippets/228_Circle_1.py new file mode 100644 index 0000000..fdf17e9 --- /dev/null +++ b/tests/snippets/228_Circle_1.py @@ -0,0 +1,22 @@ +# mcrf: objects=[Circle,Color,Scene] verified=0.2.8-dev status=ok +import mcrfpy + +scene = mcrfpy.Scene("circle_demo") +mcrfpy.current_scene = scene + +# Create a filled circle +circle = mcrfpy.Circle(radius=30, center=(100, 100)) +circle.fill_color = mcrfpy.Color(0, 255, 0) + +# Add to scene +scene.children.append(circle) + +# Circle with outline only +ring = mcrfpy.Circle( + radius=50, + center=(200, 200), + fill_color=mcrfpy.Color(0, 0, 0, 0), # Transparent fill + outline_color=mcrfpy.Color(255, 255, 255), + outline=3 +) +scene.children.append(ring) diff --git a/tests/snippets/229_Circle_2.py b/tests/snippets/229_Circle_2.py new file mode 100644 index 0000000..79f0b24 --- /dev/null +++ b/tests/snippets/229_Circle_2.py @@ -0,0 +1,29 @@ +# mcrf: objects=[Circle,Color,Scene] verified=0.2.8-dev status=ok +import mcrfpy + +scene = mcrfpy.Scene("circle_button_demo") +mcrfpy.current_scene = scene + +# Interactive button circle +button = mcrfpy.Circle( + radius=25, + center=(150, 150), + fill_color=mcrfpy.Color(50, 50, 200), + outline_color=mcrfpy.Color(100, 100, 255), + outline=2 +) + +def on_hover_enter(pos): + button.fill_color = mcrfpy.Color(80, 80, 230) + +def on_hover_exit(pos): + button.fill_color = mcrfpy.Color(50, 50, 200) + +def on_click(pos, btn, action): + print("Button clicked!") + +button.on_enter = on_hover_enter +button.on_exit = on_hover_exit +button.on_click = on_click + +scene.children.append(button) diff --git a/tests/snippets/230_ColorLayer_1.py b/tests/snippets/230_ColorLayer_1.py new file mode 100644 index 0000000..d030197 --- /dev/null +++ b/tests/snippets/230_ColorLayer_1.py @@ -0,0 +1,41 @@ +# mcrf: objects=[Caption,Color,ColorLayer,Grid,Scene] verified=0.2.8-dev status=ok +import mcrfpy + +scene = mcrfpy.Scene("demo") +mcrfpy.current_scene = scene + +grid = mcrfpy.Grid( + grid_size=(16, 12), + texture=mcrfpy.default_texture, + pos=(0, 0), + size=(1024, 768), + zoom=4.0 +) +scene.children.append(grid) + +for y in range(12): + for x in range(16): + grid.at(x, y).tilesprite = 48 + +# Create standalone, then attach to a Grid +color_layer = mcrfpy.ColorLayer(z_index=1, name="tint") +grid.add_layer(color_layer) + +# Set individual cell colors +color_layer.set((5, 5), mcrfpy.Color(255, 0, 0, 128)) # Semi-transparent red + +# Fill entire layer +color_layer.fill(mcrfpy.Color(0, 0, 0, 200)) # Dark overlay + +# Fill rectangular region +color_layer.fill_rect((2, 2), (4, 4), mcrfpy.Color(50, 50, 100)) + +# Get cell color +cell_color = color_layer.at((5, 5)) +assert isinstance(cell_color, mcrfpy.Color) + +# FOV visualization: paint cells by visibility from a source position +color_layer.draw_fov((8, 6), radius=8) + +title = mcrfpy.Caption(text="ColorLayer quick reference", pos=(300, 720)) +scene.children.append(title) diff --git a/tests/snippets/231_ColorLayer_2.py b/tests/snippets/231_ColorLayer_2.py new file mode 100644 index 0000000..1c875eb --- /dev/null +++ b/tests/snippets/231_ColorLayer_2.py @@ -0,0 +1,41 @@ +# mcrf: objects=[Caption,Color,ColorLayer,Grid,HeightMap,Scene] verified=0.2.8-dev status=ok +import mcrfpy + +scene = mcrfpy.Scene("demo") +mcrfpy.current_scene = scene + +grid = mcrfpy.Grid( + grid_size=(16, 12), + texture=mcrfpy.default_texture, + pos=(0, 0), + size=(1024, 768), + zoom=4.0 +) +scene.children.append(grid) + +for y in range(12): + for x in range(16): + grid.at(x, y).tilesprite = 48 + +heightmap = mcrfpy.HeightMap((16, 12)) +heightmap.fill(0.0) +for x in range(16): + heightmap[x, 6] = 1.0 # simple ramp so both ranges have cells + +color_layer = mcrfpy.ColorLayer(z_index=0, name="terrain") +grid.add_layer(color_layer) + +# Color by height threshold (one call per range) +color_layer.apply_threshold(heightmap, range=(0.0, 0.5), color=mcrfpy.Color(0, 0, 100)) # Water (blue) +color_layer.apply_threshold(heightmap, range=(0.5, 1.0), color=mcrfpy.Color(0, 100, 0)) # Land (green) + +# Or use a smooth gradient across the whole range +color_layer.apply_gradient( + heightmap, + range=(0.0, 1.0), + color_low=mcrfpy.Color(0, 0, 150), # Low = deep water + color_high=mcrfpy.Color(200, 200, 200) # High = mountain +) + +title = mcrfpy.Caption(text="ColorLayer heightmap visualization", pos=(280, 720)) +scene.children.append(title) diff --git a/tests/snippets/232_Color.py b/tests/snippets/232_Color.py new file mode 100644 index 0000000..9ea8b6c --- /dev/null +++ b/tests/snippets/232_Color.py @@ -0,0 +1,30 @@ +# mcrf: objects=[Caption,Color,Frame,Scene] verified=0.2.8-dev status=ok +import mcrfpy + +scene = mcrfpy.Scene("color_demo") +mcrfpy.current_scene = scene + +# Create colors +red = mcrfpy.Color(255, 0, 0) +semi_transparent = mcrfpy.Color(100, 100, 255, 128) + +# From hex string +blue = mcrfpy.Color.from_hex("#0066CC") +white = mcrfpy.Color.from_hex("FFFFFF") + +# Color interpolation +start = mcrfpy.Color(255, 0, 0) +end = mcrfpy.Color(0, 0, 255) +middle = start.lerp(end, 0.5) # Purple + +# Apply to elements +frame = mcrfpy.Frame(pos=(0, 0), size=(200, 100), fill_color=mcrfpy.Color(40, 40, 60)) +caption = mcrfpy.Caption(text="Hello", pos=(10, 10), fill_color=mcrfpy.Color(255, 255, 255)) + +scene.children.append(frame) +scene.children.append(caption) + +# Read-modify-writeback idiom (frame.fill_color.r = 255 has no effect) +color = frame.fill_color +color.r = 255 +frame.fill_color = color # Reassign to apply diff --git a/tests/snippets/233_DijkstraMap.py b/tests/snippets/233_DijkstraMap.py new file mode 100644 index 0000000..9da39c5 --- /dev/null +++ b/tests/snippets/233_DijkstraMap.py @@ -0,0 +1,61 @@ +# mcrf: objects=[Caption,Color,ColorLayer,Entity,Grid,Scene] verified=0.2.8-dev status=ok +import mcrfpy + +scene = mcrfpy.Scene("dijkstra_demo") +mcrfpy.current_scene = scene + +grid = mcrfpy.Grid( + grid_size=(16, 12), + texture=mcrfpy.default_texture, + pos=(0, 0), + size=(1024, 768), + zoom=4.0, +) +scene.children.append(grid) + +for y in range(12): + for x in range(16): + cell = grid.at(x, y) + cell.tilesprite = 48 + cell.walkable = True + +player = mcrfpy.Entity(grid_pos=(8, 6), texture=mcrfpy.default_texture, grid=grid) +enemy = mcrfpy.Entity(grid_pos=(2, 2), texture=mcrfpy.default_texture, grid=grid) + +# AI Movement Toward Goal +player_dijkstra = grid.get_dijkstra_map(player.grid_pos) + +for e in grid.entities: + if e != player: + next_step = player_dijkstra.step_from(e) + if next_step: + e.grid_pos = next_step + +# Distance-Based Decisions +goal_map = grid.get_dijkstra_map(player.grid_pos) +for e in grid.entities: + dist = goal_map.distance(e) + if dist is None: + print(f"{e} cannot reach goal") + elif dist < 5: + print(f"{e} is very close!") + else: + print(f"{e} is {dist} steps away") + +# Visualization with HeightMap +heightmap = goal_map.to_heightmap() +color_layer = mcrfpy.ColorLayer(z_index=1, name="distance_gradient") +grid.add_layer(color_layer) +color_layer.apply_gradient( + heightmap, + (0, 20), + mcrfpy.Color(0, 255, 0), + mcrfpy.Color(255, 0, 0), +) + +# Cache Management +grid.at(5, 5).walkable = False +grid.clear_dijkstra_maps() + +status = mcrfpy.Caption(text="Dijkstra distance map demo", pos=(320, 720)) +scene.children.append(status) diff --git a/tests/snippets/234_Drawable.py b/tests/snippets/234_Drawable.py new file mode 100644 index 0000000..6c99335 --- /dev/null +++ b/tests/snippets/234_Drawable.py @@ -0,0 +1,29 @@ +# mcrf: objects=[Caption,Frame,Scene,Sprite] verified=0.2.8-dev status=ok +import mcrfpy + +scene = mcrfpy.Scene("drawable_demo") +mcrfpy.current_scene = scene + +# Drawable is abstract - use subclasses +frame = mcrfpy.Frame(pos=(0, 0), size=(100, 100)) +caption = mcrfpy.Caption(text="Hello", pos=(10, 10)) +sprite = mcrfpy.Sprite(texture=mcrfpy.default_texture, pos=(50, 50)) + +# Common Drawable properties +frame.visible = True +frame.opacity = 0.5 +frame.z_index = 10 + +# Common Drawable methods +frame.move(50, 25) +frame.resize(200, 150) + +# Click handling +def on_click(pos, button, action): + print(f"Clicked at ({pos.x}, {pos.y})") + +frame.on_click = on_click + +scene.children.append(frame) +scene.children.append(caption) +scene.children.append(sprite) diff --git a/tests/snippets/235_Easing.py b/tests/snippets/235_Easing.py new file mode 100644 index 0000000..0830e30 --- /dev/null +++ b/tests/snippets/235_Easing.py @@ -0,0 +1,18 @@ +# mcrf: objects=[Easing,Scene,Sprite] verified=0.2.8-dev status=ok +import mcrfpy + +scene = mcrfpy.Scene("easing_demo") +mcrfpy.current_scene = scene + +sprite = mcrfpy.Sprite(pos=(0, 0)) +scene.children.append(sprite) + +# Use with animate() method +sprite.animate("x", 500, 1.0, mcrfpy.Easing.EASE_OUT_QUAD) +sprite.animate("opacity", 0, 0.5, mcrfpy.Easing.EASE_IN_OUT) + +# Bouncy effect +sprite.animate("scale", 1.2, 0.3, mcrfpy.Easing.EASE_OUT_ELASTIC) + +# Anticipation (pullback before action) +sprite.animate("x", 100, 0.5, mcrfpy.Easing.EASE_IN_BACK) diff --git a/tests/snippets/236_Entity.py b/tests/snippets/236_Entity.py new file mode 100644 index 0000000..925baba --- /dev/null +++ b/tests/snippets/236_Entity.py @@ -0,0 +1,42 @@ +# mcrf: objects=[Easing,Entity,Grid,Scene] verified=0.2.8-dev status=ok +import mcrfpy + +scene = mcrfpy.Scene("entity-demo") +mcrfpy.current_scene = scene + +grid = mcrfpy.Grid( + grid_size=(16, 12), + texture=mcrfpy.default_texture, + pos=(0, 0), + size=(1024, 768), + zoom=4.0 +) +scene.children.append(grid) + +for y in range(12): + for x in range(16): + grid.at(x, y).tilesprite = 48 + +# Create an entity +player = mcrfpy.Entity(grid_pos=(10, 10), texture=mcrfpy.default_texture, sprite_index=84) + +# Add to a grid +grid.entities.append(player) + +# Move to a new grid (tile) position -- instant, integer coordinates +player.grid_pos = (11, 10) + +# Animated movement: 'x' is an alias for 'draw_x', the fractional tile +# coordinate used for smooth motion between cells. +player.animate("x", 12.0, 0.2, mcrfpy.Easing.EASE_OUT_QUAD) + +# Pathfinding: find_path() returns an AStarPath (or None if unreachable); +# it does not move the entity itself. +path = player.find_path((15, 10)) +if path is not None: + print("Path found!") + +# Field of view +visible = player.visible_entities(radius=8) +for enemy in visible: + print(f"Can see: {enemy.name}") diff --git a/tests/snippets/237_FOV_1.py b/tests/snippets/237_FOV_1.py new file mode 100644 index 0000000..f0bb87b --- /dev/null +++ b/tests/snippets/237_FOV_1.py @@ -0,0 +1,26 @@ +# mcrf: objects=[Entity,FOV,Grid,Scene] verified=0.2.8-dev status=ok +import mcrfpy + +scene = mcrfpy.Scene("fov-demo") +mcrfpy.current_scene = scene + +# Create grid and set algorithm +grid = mcrfpy.Grid(grid_size=(80, 45), texture=mcrfpy.default_texture, pos=(0, 0), size=(800, 450)) +grid.fov = mcrfpy.FOV.SHADOW +grid.fov_radius = 10 +scene.children.append(grid) + +player = mcrfpy.Entity(grid_pos=(5, 5), texture=mcrfpy.default_texture, sprite_index=84) +grid.entities.append(player) + +enemies = [mcrfpy.Entity(grid_pos=(10, 10), texture=mcrfpy.default_texture, sprite_index=85)] +for enemy in enemies: + grid.entities.append(enemy) + +# Compute FOV from the player's tile position (radius defaults to grid.fov_radius when omitted) +grid.compute_fov(player.grid_pos) + +# Use in visibility checks +for enemy in enemies: + if grid.is_in_fov(enemy.grid_x, enemy.grid_y): + enemy.visible = True diff --git a/tests/snippets/238_FOV_2.py b/tests/snippets/238_FOV_2.py new file mode 100644 index 0000000..9a4a708 --- /dev/null +++ b/tests/snippets/238_FOV_2.py @@ -0,0 +1,20 @@ +# mcrf: objects=[Color,ColorLayer,Entity,Grid,Scene] verified=0.2.8-dev status=ok +import mcrfpy + +scene = mcrfpy.Scene("fov-colorlayer-demo") +mcrfpy.current_scene = scene + +grid = mcrfpy.Grid(grid_size=(20, 15), texture=mcrfpy.default_texture, pos=(0, 0), size=(640, 480)) +scene.children.append(grid) + +player = mcrfpy.Entity(grid_pos=(10, 7), texture=mcrfpy.default_texture, sprite_index=84) +grid.entities.append(player) + +fog = mcrfpy.ColorLayer(z_index=10, name="fog") +grid.add_layer(fog) +fog.fill(mcrfpy.Color(0, 0, 0, 255)) # Start dark + +def update_visibility(): + fog.draw_fov((player.grid_x, player.grid_y), radius=8) # Reveal visible cells, re-fog the rest + +update_visibility() diff --git a/tests/snippets/239_Font_1.py b/tests/snippets/239_Font_1.py new file mode 100644 index 0000000..8a8a06a --- /dev/null +++ b/tests/snippets/239_Font_1.py @@ -0,0 +1,21 @@ +# mcrf: objects=[Caption,Font,Scene] verified=0.2.8-dev status=ok +import mcrfpy + +scene = mcrfpy.Scene("font_demo") +mcrfpy.current_scene = scene + +# Load a font +font = mcrfpy.Font("assets/JetbrainsMono.ttf") + +# Use with Caption +caption = mcrfpy.Caption( + text="Hello, World!", + pos=(100, 100), + font=font +) + +scene.children.append(caption) + +# Check font properties +print(f"Font family: {font.family}") +print(f"Loaded from: {font.source}") diff --git a/tests/snippets/240_Font_2.py b/tests/snippets/240_Font_2.py new file mode 100644 index 0000000..fef7cc0 --- /dev/null +++ b/tests/snippets/240_Font_2.py @@ -0,0 +1,20 @@ +# mcrf: objects=[Caption,Color,Font,Scene] verified=0.2.8-dev status=ok +import mcrfpy + +# Load font +font = mcrfpy.Font("assets/JetbrainsMono.ttf") + +# Create a scene to hold the caption +scene = mcrfpy.Scene("title_screen") +mcrfpy.current_scene = scene + +# Create caption with font +title = mcrfpy.Caption( + text="Game Title", + pos=(400, 100), + font=font +) +title.font_size = 48 +title.fill_color = mcrfpy.Color(255, 255, 255) + +scene.children.append(title) diff --git a/tests/snippets/241_Frame.py b/tests/snippets/241_Frame.py new file mode 100644 index 0000000..42d92a1 --- /dev/null +++ b/tests/snippets/241_Frame.py @@ -0,0 +1,24 @@ +# mcrf: objects=[Caption,Color,Frame,Scene] verified=0.2.8-dev status=ok +import mcrfpy + +scene = mcrfpy.Scene("demo") +mcrfpy.current_scene = scene + +# Create a styled panel +panel = mcrfpy.Frame(pos=(100, 100), size=(200, 150)) +panel.fill_color = mcrfpy.Color(40, 40, 40, 200) +panel.outline_color = mcrfpy.Color(100, 100, 100) +panel.outline = 2 + +# Add to scene +scene.children.append(panel) + +# Frames can contain children +label = mcrfpy.Caption(text="Panel Title", pos=(10, 10)) +panel.children.append(label) + +# Enable clipping for scrollable content +panel.clip_children = True + +# Animate properties +panel.animate("opacity", 0.5, 1.0, "easeInOut") diff --git a/tests/snippets/242_Grid.py b/tests/snippets/242_Grid.py new file mode 100644 index 0000000..fd6b07e --- /dev/null +++ b/tests/snippets/242_Grid.py @@ -0,0 +1,42 @@ +# mcrf: objects=[ColorLayer,Entity,Grid,Scene,TileLayer] verified=0.2.8-dev status=ok +import mcrfpy + +scene = mcrfpy.Scene("demo") +mcrfpy.current_scene = scene + +texture = mcrfpy.default_texture + +# Create a grid +grid = mcrfpy.Grid(grid_size=(80, 45), texture=texture, pos=(0, 0), size=(800, 600)) +scene.children.append(grid) + +# Position and zoom +grid.pos = (100, 50) +grid.zoom = 2.0 +grid.center_camera((40, 22)) # Center on tile coordinates + +# Work with layers - construct a layer object, then attach it +tile_layer = mcrfpy.TileLayer(name="floor", z_index=0, texture=texture) +color_layer = mcrfpy.ColorLayer(z_index=1, name="tint") +grid.add_layer(tile_layer) +grid.add_layer(color_layer) +tile_layer.set((5, 5), 42) # Set tile sprite index + +# Add entities +player = mcrfpy.Entity(grid_pos=(10, 10), texture=texture, sprite_index=84) +grid.entities.append(player) + +# Field of view +grid.fov_radius = 8 +grid.compute_fov(player.grid_pos, radius=8) +if grid.is_in_fov((15, 10)): + print("Visible!") + +# Pathfinding +path = grid.find_path(player.grid_pos, (20, 15)) +if path: + next_step = path.walk() + +# Dijkstra maps +dijkstra = grid.get_dijkstra_map((20, 15)) +distance = dijkstra.distance((10, 10)) diff --git a/tests/snippets/243_HeightMap.py b/tests/snippets/243_HeightMap.py new file mode 100644 index 0000000..68f3153 --- /dev/null +++ b/tests/snippets/243_HeightMap.py @@ -0,0 +1,33 @@ +# mcrf: objects=[BSP,Color,ColorLayer,Grid,HeightMap,NoiseSource,Scene] verified=0.2.8-dev status=ok +import mcrfpy + +scene = mcrfpy.Scene("heightmap-demo") +mcrfpy.current_scene = scene + +grid = mcrfpy.Grid(grid_size=(20, 20)) +scene.children.append(grid) + +# Create a heightmap +hmap = mcrfpy.HeightMap((20, 20)) + +# Method chaining for terrain generation +hmap.fill(0.5).add_hill((10, 10), 8, 0.3).normalize() + +# Subscript access +value = hmap[5, 5] + +# Combine multiple heightmaps +noise_source = mcrfpy.NoiseSource(dimensions=2, algorithm='simplex', seed=42) +noise_map = noise_source.sample((20, 20)) +hmap.multiply(noise_map) # Apply as mask + +# Convert BSP to heightmap +bsp = mcrfpy.BSP(pos=(0, 0), size=(20, 20)) +bsp.split_recursive(depth=3, min_size=(4, 4)) +rooms = bsp.to_heightmap(select='leaves', shrink=1) +hmap.add(rooms) + +# Visualize the heightmap on the grid via a ColorLayer gradient +color_layer = mcrfpy.ColorLayer(z_index=1, name="height_color") +grid.add_layer(color_layer) +color_layer.apply_gradient(hmap, (0.0, 1.0), mcrfpy.Color(0, 0, 0), mcrfpy.Color(255, 255, 255)) diff --git a/tests/snippets/244_InputState.py b/tests/snippets/244_InputState.py new file mode 100644 index 0000000..71dcda1 --- /dev/null +++ b/tests/snippets/244_InputState.py @@ -0,0 +1,13 @@ +# mcrf: objects=[Frame,InputState,Scene] verified=0.2.8-dev status=ok +import mcrfpy + +def handle_input(key, state): + if state == mcrfpy.InputState.PRESSED: + print(f"{key} was pressed") + elif state == mcrfpy.InputState.RELEASED: + print(f"{key} was released") + +scene = mcrfpy.Scene("demo") +mcrfpy.current_scene = scene +scene.children.append(mcrfpy.Frame(pos=(0, 0), size=(800, 600))) +scene.on_key = handle_input diff --git a/tests/snippets/245_Key.py b/tests/snippets/245_Key.py new file mode 100644 index 0000000..7889fcf --- /dev/null +++ b/tests/snippets/245_Key.py @@ -0,0 +1,19 @@ +# mcrf: objects=[Caption,InputState,Key,Scene] verified=0.2.8-dev status=ok +import mcrfpy + +scene = mcrfpy.Scene("key_demo") + + +def handle_key(key, state): + if state == mcrfpy.InputState.PRESSED: + if key == mcrfpy.Key.ESCAPE: + print("Escape pressed") + elif key == mcrfpy.Key.SPACE: + print("Space pressed") + elif key == mcrfpy.Key.W: + print("W pressed") + + +scene.on_key = handle_key +scene.children.append(mcrfpy.Caption(text="Press a key", pos=(10, 10))) +mcrfpy.current_scene = scene diff --git a/tests/snippets/246_Keyboard.py b/tests/snippets/246_Keyboard.py new file mode 100644 index 0000000..5b851d6 --- /dev/null +++ b/tests/snippets/246_Keyboard.py @@ -0,0 +1,22 @@ +# mcrf: objects=[Caption,InputState,Key,Scene] verified=0.2.8-dev status=ok +import mcrfpy + +scene = mcrfpy.Scene("demo") +mcrfpy.current_scene = scene +scene.children.append(mcrfpy.Caption(text="Press S, Ctrl+S, or Shift+S", pos=(50, 50))) + +def handle_key(key, state): + if state == mcrfpy.InputState.PRESSED: + if key == mcrfpy.Key.S: + if mcrfpy.keyboard.ctrl: + save_game() # Ctrl+S + elif mcrfpy.keyboard.shift: + save_as() # Shift+S + else: + move_south() # Just S + +scene.on_key = handle_key + +# Check modifier states anywhere +if mcrfpy.keyboard.shift: + print("Shift is currently held") diff --git a/tests/snippets/247_Line_1.py b/tests/snippets/247_Line_1.py new file mode 100644 index 0000000..e43161a --- /dev/null +++ b/tests/snippets/247_Line_1.py @@ -0,0 +1,22 @@ +# mcrf: objects=[Color,Line,Scene] verified=0.2.8-dev status=ok +import mcrfpy + +scene = mcrfpy.Scene("line_quickref") +mcrfpy.current_scene = scene + +# Create a simple line +line = mcrfpy.Line(start=(50, 50), end=(200, 150)) +line.color = mcrfpy.Color(255, 255, 0) +line.thickness = 2 + +# Add to scene +scene.children.append(line) + +# Create a divider +divider = mcrfpy.Line( + start=(0, 100), + end=(400, 100), + thickness=1, + color=mcrfpy.Color(100, 100, 100) +) +scene.children.append(divider) diff --git a/tests/snippets/248_Line_2.py b/tests/snippets/248_Line_2.py new file mode 100644 index 0000000..dd4a336 --- /dev/null +++ b/tests/snippets/248_Line_2.py @@ -0,0 +1,30 @@ +# mcrf: objects=[Color,Easing,Line,Scene] verified=0.2.8-dev status=ok +import mcrfpy + +scene = mcrfpy.Scene("line_example") +mcrfpy.current_scene = scene + +# Draw a triangle using three lines +points = [(100, 50), (50, 150), (150, 150)] +color = mcrfpy.Color(255, 100, 100) + +for i in range(3): + line = mcrfpy.Line( + start=points[i], + end=points[(i + 1) % 3], + thickness=2, + color=color + ) + scene.children.append(line) + +# Animated growing line +growing_line = mcrfpy.Line( + start=(200, 100), + end=(200, 100), # Start with zero length + thickness=3, + color=mcrfpy.Color(0, 255, 255) +) +scene.children.append(growing_line) + +# Animate the end point to create a growing effect +growing_line.animate("end", (350, 200), 1.5, mcrfpy.Easing.EASE_OUT) diff --git a/tests/snippets/249_MouseButton_1.py b/tests/snippets/249_MouseButton_1.py new file mode 100644 index 0000000..114eda0 --- /dev/null +++ b/tests/snippets/249_MouseButton_1.py @@ -0,0 +1,31 @@ +# mcrf: objects=[Color,Frame,InputState,MouseButton,Scene] verified=0.2.8-dev status=ok +import mcrfpy + +scene = mcrfpy.Scene("mousebutton_demo") +mcrfpy.current_scene = scene + +my_button = mcrfpy.Frame( + pos=(50, 50), size=(200, 80), + fill_color=mcrfpy.Color(100, 100, 200) +) +scene.children.append(my_button) + + +def activate_button(): + print("activated") + + +def show_context_menu(x, y): + print(f"context menu at ({x}, {y})") + + +def button_click(pos, button, action): + if action != mcrfpy.InputState.PRESSED: + return + if button == mcrfpy.MouseButton.LEFT: + activate_button() + elif button == mcrfpy.MouseButton.RIGHT: + show_context_menu(pos.x, pos.y) + + +my_button.on_click = button_click diff --git a/tests/snippets/250_MouseButton_2.py b/tests/snippets/250_MouseButton_2.py new file mode 100644 index 0000000..7483768 --- /dev/null +++ b/tests/snippets/250_MouseButton_2.py @@ -0,0 +1,45 @@ +# mcrf: objects=[Grid,InputState,MouseButton,Scene] verified=0.2.8-dev status=ok +import mcrfpy + +scene = mcrfpy.Scene("mousebutton_grid_demo") +mcrfpy.current_scene = scene + +grid = mcrfpy.Grid(grid_size=(10, 10), pos=(0, 0), size=(400, 400)) +scene.children.append(grid) + + +def select_at(x, y): + print(f"select at ({x}, {y})") + + +def move_to(x, y): + print(f"move to ({x}, {y})") + + +def start_pan(x, y): + print(f"pan from ({x}, {y})") + + +def go_back(): + print("back") + + +def go_forward(): + print("forward") + + +def on_cell_click(cell_pos, button, action): + if action == mcrfpy.InputState.PRESSED: + if button == mcrfpy.MouseButton.LEFT: + select_at(cell_pos.x, cell_pos.y) + elif button == mcrfpy.MouseButton.RIGHT: + move_to(cell_pos.x, cell_pos.y) + elif button == mcrfpy.MouseButton.MIDDLE: + start_pan(cell_pos.x, cell_pos.y) + elif button == mcrfpy.MouseButton.X1: + go_back() + elif button == mcrfpy.MouseButton.X2: + go_forward() + + +grid.on_cell_click = on_cell_click diff --git a/tests/snippets/251_Mouse_1.py b/tests/snippets/251_Mouse_1.py new file mode 100644 index 0000000..aa7f4c4 --- /dev/null +++ b/tests/snippets/251_Mouse_1.py @@ -0,0 +1,22 @@ +# mcrf: objects=[Caption,Scene] verified=0.2.8-dev status=ok +import mcrfpy + +scene = mcrfpy.Scene("mouse_quick_ref") +mcrfpy.current_scene = scene + +caption = mcrfpy.Caption(text="Mouse Quick Reference", pos=(50, 50)) +scene.children.append(caption) + +# Check mouse position +x, y = mcrfpy.mouse.pos +print(f"Mouse at ({x}, {y})") + +# Check button states +if mcrfpy.mouse.left: + print("Left button is held") + +# Hide cursor for custom cursor sprite +mcrfpy.mouse.visible = False + +# Grab mouse for camera control +mcrfpy.mouse.grabbed = True diff --git a/tests/snippets/252_Mouse_2.py b/tests/snippets/252_Mouse_2.py new file mode 100644 index 0000000..724f2cd --- /dev/null +++ b/tests/snippets/252_Mouse_2.py @@ -0,0 +1,19 @@ +# mcrf: objects=[Scene,Sprite,Timer] verified=0.2.8-dev status=ok +import mcrfpy + +scene = mcrfpy.Scene("custom_cursor") +mcrfpy.current_scene = scene + +# Create a custom cursor sprite (uses the default texture atlas) +cursor = mcrfpy.Sprite(pos=(0, 0), sprite_index=1) +scene.children.append(cursor) + +# Hide the system cursor +mcrfpy.mouse.visible = False + +# Update the cursor's position every frame via a Timer +def update(timer, runtime): + pos = mcrfpy.mouse.pos + cursor.x, cursor.y = pos.x, pos.y + +mcrfpy.Timer("cursor_follow", update, 16) diff --git a/tests/snippets/253_Music.py b/tests/snippets/253_Music.py new file mode 100644 index 0000000..d7e5eaf --- /dev/null +++ b/tests/snippets/253_Music.py @@ -0,0 +1,21 @@ +# mcrf: objects=[Caption,Music,Scene] verified=0.2.8-dev status=ok +import mcrfpy + +scene = mcrfpy.Scene("music_demo") +scene.children.append(mcrfpy.Caption(text="Music demo", pos=(20, 20))) +mcrfpy.current_scene = scene + +# Load and play background music +music = mcrfpy.Music("assets/sfx/splat1.ogg") +music.volume = 50 # 50% volume +music.loop = True +music.play() + +# Control playback +music.pause() +music.position = 0.0 # Jump to a position in seconds +music.play() + +# Check status +print(f"Duration: {music.duration}s") +print(f"Playing: {music.playing}") diff --git a/tests/snippets/254_NoiseSource_1.py b/tests/snippets/254_NoiseSource_1.py new file mode 100644 index 0000000..2a801ae --- /dev/null +++ b/tests/snippets/254_NoiseSource_1.py @@ -0,0 +1,23 @@ +# mcrf: objects=[Caption,NoiseSource,Scene] verified=0.2.8-dev status=ok +import mcrfpy + +# Create a noise source +noise = mcrfpy.NoiseSource(dimensions=2, algorithm='simplex', seed=42) + +# Point queries +value = noise.get((10.5, 20.3)) # Basic noise: -1.0 to 1.0 +fbm_val = noise.fbm((10.5, 20.3), octaves=6) # Fractal brownian motion +turb_val = noise.turbulence((10.5, 20.3)) # Turbulence (absolute fbm) + +# Batch sampling into HeightMap +hmap = noise.sample((100, 100), mode='fbm', octaves=4) + +# Direct heightmap integration +hmap.add_noise(noise, scale=0.5) + +scene = mcrfpy.Scene("noise_demo") +mcrfpy.current_scene = scene +scene.children.append(mcrfpy.Caption( + text=f"noise={value:.3f} fbm={fbm_val:.3f} turb={turb_val:.3f}", + pos=(10, 10) +)) diff --git a/tests/snippets/255_NoiseSource_2.py b/tests/snippets/255_NoiseSource_2.py new file mode 100644 index 0000000..d44da4f --- /dev/null +++ b/tests/snippets/255_NoiseSource_2.py @@ -0,0 +1,32 @@ +# mcrf: objects=[Caption,NoiseSource,Scene] verified=0.2.8-dev status=ok +import mcrfpy + +# Create terrain with multiple noise layers +base_noise = mcrfpy.NoiseSource(seed=12345) +detail_noise = mcrfpy.NoiseSource(seed=67890, lacunarity=3.0) + +# Start with base terrain +terrain = base_noise.sample((100, 100), + world_size=(10.0, 10.0), # Larger scale features + mode='fbm', + octaves=4 +) + +# Normalize to 0-1 range +terrain.add_constant(1.0).scale(0.5) + +# Add detail layer +terrain.add_noise(detail_noise, + world_size=(50.0, 50.0), # Finer detail + scale=0.2 +) + +# Apply erosion and smoothing +terrain.rain_erosion(1000).smooth(2) + +scene = mcrfpy.Scene("terrain_demo") +mcrfpy.current_scene = scene +scene.children.append(mcrfpy.Caption( + text=f"terrain size={terrain.size}", + pos=(10, 10) +)) diff --git a/tests/snippets/256_Scene_1.py b/tests/snippets/256_Scene_1.py new file mode 100644 index 0000000..df8a4b0 --- /dev/null +++ b/tests/snippets/256_Scene_1.py @@ -0,0 +1,44 @@ +# mcrf: objects=[Caption,Frame,InputState,Key,Scene,Transition] verified=0.2.8-dev status=ok +import mcrfpy + +# Create a scene +scene = mcrfpy.Scene("game") + +# Add UI elements +scene.children.append(mcrfpy.Frame(pos=(0, 0), size=(800, 600))) +scene.children.append(mcrfpy.Caption(text="Hello", pos=(100, 100))) + +menu_scene = mcrfpy.Scene("menu") + +# Handle input +def on_key(key, action): + if key == mcrfpy.Key.ESCAPE and action == mcrfpy.InputState.PRESSED: + menu_scene.activate() + +scene.on_key = on_key + +# Activate with optional transition +scene.activate() +scene.activate(mcrfpy.Transition.FADE, duration=0.5) + +mcrfpy.current_scene = scene + +# Subclass for lifecycle callbacks +class GameScene(mcrfpy.Scene): + def on_enter(self): + print("Entering game scene") + + def on_exit(self): + print("Leaving game scene") + + def on_key(self, key, action): + if key == mcrfpy.Key.Q and action == mcrfpy.InputState.PRESSED: + self.handle_quit() + + def update(self, dt): + # Called every frame with delta time + self.update_entities(dt) + + def on_resize(self, new_size): + # Window resize handling - new_size is a Vector(width, height) + self.realign() diff --git a/tests/snippets/257_Scene_2.py b/tests/snippets/257_Scene_2.py new file mode 100644 index 0000000..e957de7 --- /dev/null +++ b/tests/snippets/257_Scene_2.py @@ -0,0 +1,18 @@ +# mcrf: objects=[Caption,InputState,Key,Scene] verified=0.2.8-dev status=ok +import mcrfpy + +# Create and populate a scene +menu = mcrfpy.Scene("menu") +menu.children.append(mcrfpy.Caption( + text="Press ENTER to start", + pos=(400, 300) +)) + +def menu_keys(key, action): + if key == mcrfpy.Key.ENTER and action == mcrfpy.InputState.PRESSED: + mcrfpy.Scene("game").activate() + +menu.on_key = menu_keys +menu.activate() + +mcrfpy.current_scene = menu diff --git a/tests/snippets/258_Sprite.py b/tests/snippets/258_Sprite.py new file mode 100644 index 0000000..46909a3 --- /dev/null +++ b/tests/snippets/258_Sprite.py @@ -0,0 +1,25 @@ +# mcrf: objects=[Scene,Sprite] verified=0.2.8-dev status=ok +# Sprite Quick Reference - create, scale, animate a sprite +import mcrfpy + +scene = mcrfpy.Scene("demo") +mcrfpy.current_scene = scene + +# Load a texture atlas +texture = mcrfpy.default_texture + +# Create a sprite from the texture +sprite = mcrfpy.Sprite(pos=(100, 100), texture=texture, sprite_index=42) + +# Scale it +sprite.scale = 2.0 # Uniform scaling + +# Or scale independently +sprite.scale_x = 2.0 +sprite.scale_y = 1.5 + +# Add to scene +scene.children.append(sprite) + +# Animate through sprite frames +sprite.animate("sprite_index", 45, 0.5) # Animate to frame 45 diff --git a/tests/snippets/259_Texture_1.py b/tests/snippets/259_Texture_1.py new file mode 100644 index 0000000..ccc1c22 --- /dev/null +++ b/tests/snippets/259_Texture_1.py @@ -0,0 +1,16 @@ +# mcrf: objects=[Scene,Sprite,Texture] verified=0.2.8-dev status=ok +import mcrfpy + +# Load a sprite sheet +texture = mcrfpy.Texture("assets/kenney_tinydungeon.png", 16, 16) + +# Create sprites using different indices +floor = mcrfpy.Sprite(pos=(0, 0), texture=texture, sprite_index=0) +wall = mcrfpy.Sprite(pos=(16, 0), texture=texture, sprite_index=1) +player = mcrfpy.Sprite(pos=(32, 0), texture=texture, sprite_index=84) + +scene = mcrfpy.Scene("texture_demo") +scene.children.append(floor) +scene.children.append(wall) +scene.children.append(player) +mcrfpy.current_scene = scene diff --git a/tests/snippets/260_Texture_2.py b/tests/snippets/260_Texture_2.py new file mode 100644 index 0000000..da5bd47 --- /dev/null +++ b/tests/snippets/260_Texture_2.py @@ -0,0 +1,30 @@ +# mcrf: objects=[Entity,Grid,Scene,Texture,TileLayer] verified=0.2.8-dev status=ok +import mcrfpy + +# Load texture +texture = mcrfpy.Texture("assets/kenney_tinydungeon.png", 16, 16) + +# Create grid using texture +grid = mcrfpy.Grid( + grid_size=(40, 30), + texture=texture, + pos=(0, 0), + size=(640, 480) +) + +# Set tile sprites via a TileLayer +layer = mcrfpy.TileLayer(name="floor", z_index=0, texture=texture) +grid.add_layer(layer) +layer.fill(0) # Floor tile + +# Add entity with same texture +player = mcrfpy.Entity( + grid_pos=(5, 5), + texture=texture, + sprite_index=84 # Player sprite +) +grid.entities.append(player) + +scene = mcrfpy.Scene("texture_grid_demo") +scene.children.append(grid) +mcrfpy.current_scene = scene diff --git a/tests/snippets/261_TileLayer.py b/tests/snippets/261_TileLayer.py new file mode 100644 index 0000000..7829311 --- /dev/null +++ b/tests/snippets/261_TileLayer.py @@ -0,0 +1,33 @@ +# mcrf: objects=[Grid,Scene,TileLayer] verified=0.2.8-dev status=ok +import mcrfpy + +scene = mcrfpy.Scene("tilelayer_demo") +mcrfpy.current_scene = scene + +grid = mcrfpy.Grid( + grid_size=(16, 12), + texture=mcrfpy.default_texture, + pos=(0, 0), + size=(512, 384), +) +scene.children.append(grid) + +# Create standalone, then attach with Grid.add_layer() (a layer OBJECT, not a string) +floor_layer = mcrfpy.TileLayer(z_index=0, texture=mcrfpy.default_texture) +grid.add_layer(floor_layer) + +wall_layer = mcrfpy.TileLayer(z_index=1, texture=mcrfpy.default_texture) +grid.add_layer(wall_layer) + +# Fill entire layer with one tile +floor_layer.fill(0) + +# Set individual tile sprites +floor_layer.set((5, 5), 42) + +# Fill rectangular region +wall_layer.fill_rect((0, 0), (16, 1), 16) # Top wall + +# Get tile at position +sprite_idx = floor_layer.at((5, 5)) +assert sprite_idx == 42 diff --git a/tests/snippets/262_Timer.py b/tests/snippets/262_Timer.py new file mode 100644 index 0000000..a8eb15b --- /dev/null +++ b/tests/snippets/262_Timer.py @@ -0,0 +1,35 @@ +# mcrf: objects=[Color,Frame,Scene,Timer] verified=0.2.8-dev status=ok +import mcrfpy + +scene = mcrfpy.Scene("demo") +mcrfpy.current_scene = scene + +scene.children.append(mcrfpy.Frame( + pos=(0, 0), size=(1024, 768), + fill_color=mcrfpy.Color(30, 30, 40) +)) + +# Create a repeating timer (fires every 500ms) +def on_tick(timer, runtime): + print(f"Tick at {runtime}ms") + +timer = mcrfpy.Timer("game_tick", on_tick, 500) + +# One-shot timer (fires once after 2 seconds) +def delayed_action(timer, runtime): + print("Delayed action executed!") + +mcrfpy.Timer("delay", delayed_action, 2000, once=True) + +# Control timer state +timer.pause() +timer.resume() +timer.restart() +timer.stop() + +# Create paused (start manually later) +def callback(timer, runtime): + pass + +timer2 = mcrfpy.Timer("manual", callback, 1000, start=False) +timer2.start() diff --git a/tests/snippets/263_Transition_1.py b/tests/snippets/263_Transition_1.py new file mode 100644 index 0000000..b9a87cb --- /dev/null +++ b/tests/snippets/263_Transition_1.py @@ -0,0 +1,23 @@ +# mcrf: objects=[Caption,Scene,Transition] verified=0.2.8-dev status=ok +import mcrfpy + +# Create scenes +menu = mcrfpy.Scene("menu") +game = mcrfpy.Scene("game") +settings = mcrfpy.Scene("settings") + +menu.children.append(mcrfpy.Caption(text="Menu", pos=(10, 10))) +game.children.append(mcrfpy.Caption(text="Game", pos=(10, 10))) +settings.children.append(mcrfpy.Caption(text="Settings", pos=(10, 10))) + +# Fade transition (default feel) +game.activate(mcrfpy.Transition.FADE) + +# Slide transitions for navigation +settings.activate(mcrfpy.Transition.SLIDE_LEFT) + +# Instant switch (no transition) +menu.activate(mcrfpy.Transition.NONE) + +# With custom duration (seconds) +game.activate(mcrfpy.Transition.FADE, duration=0.5) diff --git a/tests/snippets/264_Transition_2.py b/tests/snippets/264_Transition_2.py new file mode 100644 index 0000000..61922c7 --- /dev/null +++ b/tests/snippets/264_Transition_2.py @@ -0,0 +1,38 @@ +# mcrf: objects=[Caption,Scene,Transition] verified=0.2.8-dev status=ok +import mcrfpy + + +class GameScene(mcrfpy.Scene): + def __init__(self): + super().__init__("game") + self.setup_ui() + + def setup_ui(self): + self.children.append(mcrfpy.Caption(text="Game", pos=(10, 10))) + + def on_enter(self): + """Called after transition completes.""" + print("Game scene active!") + + def on_exit(self): + """Called before transition to another scene.""" + print("Leaving game scene...") + + +class MenuScene(mcrfpy.Scene): + def __init__(self, game_scene): + super().__init__("menu") + self.game_scene = game_scene + self.setup_menu() + + def setup_menu(self): + self.children.append(mcrfpy.Caption(text="Menu", pos=(10, 10))) + + def start_game(self): + self.game_scene.activate(mcrfpy.Transition.FADE, duration=0.5) + + +# Usage +game = GameScene() +menu = MenuScene(game) +menu.activate() # Start at menu diff --git a/tests/snippets/265_Traversal.py b/tests/snippets/265_Traversal.py new file mode 100644 index 0000000..df72aad --- /dev/null +++ b/tests/snippets/265_Traversal.py @@ -0,0 +1,48 @@ +# mcrf: objects=[BSP,Caption,Scene,Traversal] verified=0.2.8-dev status=ok +# Traversal - BSP traversal order enumeration, including string shortcuts +import mcrfpy + +scene = mcrfpy.Scene("demo") +mcrfpy.current_scene = scene + +bsp = mcrfpy.BSP(pos=(0, 0), size=(32, 24)) +bsp.split_recursive(depth=3, min_size=(4, 4)) + +# Iterate BSP nodes in different orders +pre_order_positions = [] +for node in bsp.traverse(mcrfpy.Traversal.PRE_ORDER): + pre_order_positions.append(node.pos) + +# Level order (default) - breadth-first +level_order_count = 0 +for node in bsp.traverse(mcrfpy.Traversal.LEVEL_ORDER): + level_order_count += 1 + +# String shortcuts also work +post_order_count = 0 +for node in bsp.traverse('post'): + post_order_count += 1 + +# Using traverse with POST_ORDER ensures we see leaves before parents +leaves = [] +for node in bsp.traverse(mcrfpy.Traversal.POST_ORDER): + if node.is_leaf: + leaves.append(node) + +# Or simply iterate the BSP directly (uses leaf order) +leaves_direct = list(bsp) + +# Process rooms from smallest (deepest) to largest, connecting child regions +corridor_count = 0 +for node in bsp.traverse(mcrfpy.Traversal.INVERTED_LEVEL_ORDER): + if not node.is_leaf: + left_center = node.left.center() + right_center = node.right.center() + corridor_count += 1 + +status = mcrfpy.Caption( + text=(f"Traversal: {level_order_count} nodes, {len(leaves)} leaves, " + f"{corridor_count} corridors"), + pos=(10, 10) +) +scene.children.append(status) diff --git a/tests/snippets/266_Vector_1.py b/tests/snippets/266_Vector_1.py new file mode 100644 index 0000000..d8d4337 --- /dev/null +++ b/tests/snippets/266_Vector_1.py @@ -0,0 +1,31 @@ +# mcrf: objects=[Caption,Color,Frame,Scene,Vector] verified=0.2.8-dev status=ok +import mcrfpy + +scene = mcrfpy.Scene("vector-demo") +mcrfpy.current_scene = scene +scene.children.append(mcrfpy.Frame(pos=(0, 0), size=(1024, 768), fill_color=mcrfpy.Color(30, 30, 40))) + +# Create vectors +pos = mcrfpy.Vector(100, 200) +direction = mcrfpy.Vector(1, 0) # Unit vector pointing right + +# Arithmetic operations +new_pos = pos + direction * 50 +offset = mcrfpy.Vector(10, 10) +pos += offset + +# Distance and magnitude +other_pos = mcrfpy.Vector(150, 220) +dist = pos.distance_to(other_pos) +length = direction.magnitude() +unit = direction.normalize() + +# Use as dict key +positions = {} +positions[pos.int] = "player" + +cap = mcrfpy.Caption( + text=f"pos={pos.x:.1f},{pos.y:.1f} dist={dist:.2f} len={length:.2f} unit=({unit.x:.1f},{unit.y:.1f})", + pos=(20, 20), +) +scene.children.append(cap) diff --git a/tests/snippets/267_Vector_2.py b/tests/snippets/267_Vector_2.py new file mode 100644 index 0000000..4152d5b --- /dev/null +++ b/tests/snippets/267_Vector_2.py @@ -0,0 +1,33 @@ +# mcrf: objects=[Caption,Color,Frame,Scene,Vector] verified=0.2.8-dev status=ok +import mcrfpy + +scene = mcrfpy.Scene("vector-arithmetic") +mcrfpy.current_scene = scene +scene.children.append(mcrfpy.Frame(pos=(0, 0), size=(1024, 768), fill_color=mcrfpy.Color(30, 30, 40))) + +a = mcrfpy.Vector(10, 20) +b = mcrfpy.Vector(5, 10) + +# Addition and subtraction +c = a + b # Vector(15, 30) +d = a - b # Vector(5, 10) + +# Scalar multiplication and division +e = a * 2 # Vector(20, 40) +f = a / 2 # Vector(5, 10) + +# In-place operations +a += b +a -= b +a *= 2 +a /= 2 + +# Comparison +lines = [] +if a == b: + lines.append("Same position") +if a != b: + lines.append("Different positions") + +cap = mcrfpy.Caption(text=", ".join(lines) or "no comparisons matched", pos=(20, 20)) +scene.children.append(cap) diff --git a/tests/snippets/268_Window.py b/tests/snippets/268_Window.py new file mode 100644 index 0000000..ffbf413 --- /dev/null +++ b/tests/snippets/268_Window.py @@ -0,0 +1,21 @@ +# mcrf: objects=[Caption,Scene,Window] verified=0.2.8-dev status=ok +import mcrfpy + +# Get window instance +window = mcrfpy.Window.get() + +# Configure window (headless-safe subset; resolution/fullscreen/vsync/center() +# require a real window and raise RuntimeError in --headless mode) +window.title = "My Roguelike" +window.framerate_limit = 60 + +# Scaling modes for different resolutions +window.game_resolution = (320, 240) # Internal game resolution +window.scaling_mode = "fit" # Scale to fit window + +# Take a screenshot (returns raw RGBA bytes when no filename is given) +data = window.screenshot() + +scene = mcrfpy.Scene("window_demo") +mcrfpy.current_scene = scene +scene.children.append(mcrfpy.Caption(text=f"Window: {window.title}", pos=(50, 50))) diff --git a/tests/snippets/269_part_00_setup.py b/tests/snippets/269_part_00_setup.py new file mode 100644 index 0000000..b30a909 --- /dev/null +++ b/tests/snippets/269_part_00_setup.py @@ -0,0 +1,26 @@ +# mcrf: objects=[Caption,Color,Scene] verified=0.2.8-dev status=ok +"""McRogueFace Tutorial - Part 0: Hello Roguelike + +This script demonstrates the basic structure of a McRogueFace program. +""" +import mcrfpy + +# Create a Scene object - this is the preferred approach +scene = mcrfpy.Scene("hello") + +# Create a caption to display text +title = mcrfpy.Caption( + pos=(512, 300), + text="Hello, Roguelike!" +) +title.fill_color = mcrfpy.Color(255, 255, 255) +title.font_size = 32 + +# Add the caption to the scene's UI collection +scene.children.append(title) + +# Activate the scene to display it +scene.activate() + +# Note: There is no run() function! +# The engine is already running - your script is imported by it. diff --git a/tests/snippets/270_part_01_grid_movement.py b/tests/snippets/270_part_01_grid_movement.py new file mode 100644 index 0000000..ec216b1 --- /dev/null +++ b/tests/snippets/270_part_01_grid_movement.py @@ -0,0 +1,116 @@ +# mcrf: objects=[Caption,Color,Entity,Grid,InputState,Key,Scene,Texture] verified=0.2.8-dev status=ok +"""McRogueFace Tutorial - Part 1: The '@' and the Dungeon Grid + +Learn to create a grid, place a player entity, and handle movement. +""" +import mcrfpy + +# Sprite indices for CP437 tileset +SPRITE_AT = 64 # '@' - player character +SPRITE_FLOOR = 46 # '.' - floor tile + +# Grid dimensions (in tiles) +GRID_WIDTH = 20 +GRID_HEIGHT = 15 + +# Create the scene +scene = mcrfpy.Scene("game") + +# Load the texture (sprite sheet) +# Parameters: path, sprite_width, sprite_height +texture = mcrfpy.Texture("assets/kenney_tinydungeon.png", 16, 16) + +# Create the grid +# The grid displays tiles and contains entities +# Note: zoom must be set in the constructor to ensure correct initial camera position +grid = mcrfpy.Grid( + pos=(100, 80), # Position on screen (pixels) + size=(640, 480), # Display size (pixels) + grid_size=(GRID_WIDTH, GRID_HEIGHT), # Size in tiles + texture=texture, + zoom=2.0 # Zoom level (set here, not after, for correct camera!) +) + +# Fill the grid with floor tiles +for y in range(GRID_HEIGHT): + for x in range(GRID_WIDTH): + cell = grid.at(x, y) + cell.tilesprite = SPRITE_FLOOR + +# Create the player entity at the center of the grid +player = mcrfpy.Entity( + grid_pos=(GRID_WIDTH // 2, GRID_HEIGHT // 2), # Grid coordinates, not pixels! + texture=texture, + sprite_index=SPRITE_AT +) + +# Add the player to the grid +# Option 1: Use the grid parameter in constructor +# player = mcrfpy.Entity(grid_pos=(10, 7), texture=texture, sprite_index=SPRITE_AT, grid=grid) + +# Option 2: Append to grid.entities (what we will use) +grid.entities.append(player) + +# Add the grid to the scene +scene.children.append(grid) + +# Add a title caption +title = mcrfpy.Caption( + pos=(100, 20), + text="Part 1: Grid Movement - Use Arrow Keys or WASD" +) +title.fill_color = mcrfpy.Color(255, 255, 255) +title.font_size = 18 +scene.children.append(title) + +# Add a position display +pos_display = mcrfpy.Caption( + pos=(100, 50), + text=f"Player Position: ({player.grid_x}, {player.grid_y})" +) +pos_display.fill_color = mcrfpy.Color(200, 200, 100) +pos_display.font_size = 16 +scene.children.append(pos_display) + +def handle_keys(key: mcrfpy.Key, action: mcrfpy.InputState) -> None: + """Handle keyboard input to move the player. + + Args: + key: The mcrfpy.Key enum value that was pressed + action: mcrfpy.InputState.PRESSED or mcrfpy.InputState.RELEASED + """ + # Only respond to key press, not release + if action != mcrfpy.InputState.PRESSED: + return + + # Get current player position + px, py = player.grid_x, player.grid_y + + # Calculate new position based on key + if key == mcrfpy.Key.W or key == mcrfpy.Key.UP: + py -= 1 # Up decreases Y + elif key == mcrfpy.Key.S or key == mcrfpy.Key.DOWN: + py += 1 # Down increases Y + elif key == mcrfpy.Key.A or key == mcrfpy.Key.LEFT: + px -= 1 # Left decreases X + elif key == mcrfpy.Key.D or key == mcrfpy.Key.RIGHT: + px += 1 # Right increases X + elif key == mcrfpy.Key.ESCAPE: + mcrfpy.exit() + return + + # Update player position + player.grid_x = px + player.grid_y = py + + # Update the position display + pos_display.text = f"Player Position: ({player.grid_x}, {player.grid_y})" + +# Set the key handler on the scene +# This is the preferred approach - works on ANY scene, not just the active one +scene.on_key = handle_keys + +# Activate the scene +scene.activate() + +print("Part 1 loaded! Use WASD or Arrow keys to move.") diff --git a/tests/snippets/271_part_02_tiles_collision_1.py b/tests/snippets/271_part_02_tiles_collision_1.py new file mode 100644 index 0000000..b71c2f3 --- /dev/null +++ b/tests/snippets/271_part_02_tiles_collision_1.py @@ -0,0 +1,202 @@ +# mcrf: objects=[Caption,Color,Entity,Grid,InputState,Key,Scene,Texture] verified=0.2.8-dev status=ok +"""McRogueFace Tutorial - Part 2: Walls, Floors, and Collision + +Learn to create maps with impassable walls and collision detection. +""" +import mcrfpy + +# ============================================================================= +# Constants +# ============================================================================= + +# Sprite indices for CP437 tileset +SPRITE_WALL = 35 # '#' - wall +SPRITE_FLOOR = 46 # '.' - floor +SPRITE_PLAYER = 64 # '@' - player + +# Grid dimensions +GRID_WIDTH = 30 +GRID_HEIGHT = 20 + +# ============================================================================= +# Map Creation +# ============================================================================= + +def create_map(grid: mcrfpy.Grid) -> None: + """Fill the grid with walls and floors. + + Creates a simple room with walls around the edges and floor in the middle. + """ + for y in range(GRID_HEIGHT): + for x in range(GRID_WIDTH): + cell = grid.at(x, y) + + # Place walls around the edges + if x == 0 or x == GRID_WIDTH - 1 or y == 0 or y == GRID_HEIGHT - 1: + cell.tilesprite = SPRITE_WALL + cell.walkable = False + else: + cell.tilesprite = SPRITE_FLOOR + cell.walkable = True + + # Add some interior walls to make it interesting + # Vertical wall + for y in range(5, 15): + cell = grid.at(10, y) + cell.tilesprite = SPRITE_WALL + cell.walkable = False + + # Horizontal wall + for x in range(15, 25): + cell = grid.at(x, 10) + cell.tilesprite = SPRITE_WALL + cell.walkable = False + + # Leave gaps for doorways + grid.at(10, 10).tilesprite = SPRITE_FLOOR + grid.at(10, 10).walkable = True + grid.at(20, 10).tilesprite = SPRITE_FLOOR + grid.at(20, 10).walkable = True + +# ============================================================================= +# Collision Detection +# ============================================================================= + +def can_move_to(grid: mcrfpy.Grid, x: int, y: int) -> bool: + """Check if a position is valid for movement. + + Args: + grid: The game grid + x: Target X coordinate (in tiles) + y: Target Y coordinate (in tiles) + + Returns: + True if the position is walkable, False otherwise + """ + # Check grid bounds first + if x < 0 or x >= GRID_WIDTH: + return False + if y < 0 or y >= GRID_HEIGHT: + return False + + # Check if the tile is walkable + cell = grid.at(x, y) + return cell.walkable + +# ============================================================================= +# Game Setup +# ============================================================================= + +# Create the scene +scene = mcrfpy.Scene("game") + +# Load texture +texture = mcrfpy.Texture("assets/kenney_tinydungeon.png", 16, 16) + +# Create the grid +grid = mcrfpy.Grid( + pos=(80, 100), + size=(720, 480), + grid_size=(GRID_WIDTH, GRID_HEIGHT), + texture=texture, + zoom=1.5 +) + +# Build the map +create_map(grid) + +# Create the player in the center of the left room +player = mcrfpy.Entity( + grid_pos=(5, 10), + texture=texture, + sprite_index=SPRITE_PLAYER +) +grid.entities.append(player) + +# Add grid to scene +scene.children.append(grid) + +# ============================================================================= +# UI Elements +# ============================================================================= + +title = mcrfpy.Caption( + pos=(80, 20), + text="Part 2: Walls and Collision" +) +title.fill_color = mcrfpy.Color(255, 255, 255) +title.font_size = 24 +scene.children.append(title) + +instructions = mcrfpy.Caption( + pos=(80, 55), + text="WASD or Arrow Keys to move | Walls block movement" +) +instructions.fill_color = mcrfpy.Color(180, 180, 180) +instructions.font_size = 16 +scene.children.append(instructions) + +pos_display = mcrfpy.Caption( + pos=(80, 600), + text=f"Position: ({player.grid_x}, {player.grid_y})" +) +pos_display.fill_color = mcrfpy.Color(200, 200, 100) +pos_display.font_size = 16 +scene.children.append(pos_display) + +status_display = mcrfpy.Caption( + pos=(400, 600), + text="Status: Ready" +) +status_display.fill_color = mcrfpy.Color(100, 200, 100) +status_display.font_size = 16 +scene.children.append(status_display) + +# ============================================================================= +# Input Handling +# ============================================================================= + +def handle_keys(key: mcrfpy.Key, action: mcrfpy.InputState) -> None: + """Handle keyboard input with collision detection.""" + if action != mcrfpy.InputState.PRESSED: + return + + # Get current position + px, py = player.grid_x, player.grid_y + + # Calculate intended new position + new_x, new_y = px, py + + if key == mcrfpy.Key.W or key == mcrfpy.Key.UP: + new_y -= 1 + elif key == mcrfpy.Key.S or key == mcrfpy.Key.DOWN: + new_y += 1 + elif key == mcrfpy.Key.A or key == mcrfpy.Key.LEFT: + new_x -= 1 + elif key == mcrfpy.Key.D or key == mcrfpy.Key.RIGHT: + new_x += 1 + elif key == mcrfpy.Key.ESCAPE: + mcrfpy.exit() + return + else: + return # Ignore other keys + + # Check collision before moving + if can_move_to(grid, new_x, new_y): + player.grid_x = new_x + player.grid_y = new_y + pos_display.text = f"Position: ({new_x}, {new_y})" + status_display.text = "Status: Moved" + status_display.fill_color = mcrfpy.Color(100, 200, 100) + else: + status_display.text = "Status: Blocked!" + status_display.fill_color = mcrfpy.Color(200, 100, 100) + +scene.on_key = handle_keys + +# ============================================================================= +# Start the Game +# ============================================================================= + +scene.activate() +print("Part 2 loaded! Try walking into walls.") diff --git a/tests/snippets/272_part_02_tiles_collision_2.py b/tests/snippets/272_part_02_tiles_collision_2.py new file mode 100644 index 0000000..440aefe --- /dev/null +++ b/tests/snippets/272_part_02_tiles_collision_2.py @@ -0,0 +1,106 @@ +# mcrf: objects=[Caption,Color,Entity,Grid,InputState,Key,Scene,Texture] verified=0.2.8-dev status=ok +"""McRogueFace Tutorial - Part 2: Walls, Floors, and Collision""" +import mcrfpy + +# Constants +SPRITE_WALL = 35 +SPRITE_FLOOR = 46 +SPRITE_PLAYER = 64 +GRID_WIDTH = 30 +GRID_HEIGHT = 20 + +def create_map(grid: mcrfpy.Grid) -> None: + for y in range(GRID_HEIGHT): + for x in range(GRID_WIDTH): + cell = grid.at(x, y) + if x == 0 or x == GRID_WIDTH - 1 or y == 0 or y == GRID_HEIGHT - 1: + cell.tilesprite = SPRITE_WALL + cell.walkable = False + else: + cell.tilesprite = SPRITE_FLOOR + cell.walkable = True + + for y in range(5, 15): + cell = grid.at(10, y) + cell.tilesprite = SPRITE_WALL + cell.walkable = False + + for x in range(15, 25): + cell = grid.at(x, 10) + cell.tilesprite = SPRITE_WALL + cell.walkable = False + + grid.at(10, 10).tilesprite = SPRITE_FLOOR + grid.at(10, 10).walkable = True + grid.at(20, 10).tilesprite = SPRITE_FLOOR + grid.at(20, 10).walkable = True + +def can_move_to(grid: mcrfpy.Grid, x: int, y: int) -> bool: + if x < 0 or x >= GRID_WIDTH or y < 0 or y >= GRID_HEIGHT: + return False + return grid.at(x, y).walkable + +scene = mcrfpy.Scene("game") +texture = mcrfpy.Texture("assets/kenney_tinydungeon.png", 16, 16) + +grid = mcrfpy.Grid( + pos=(80, 100), size=(720, 480), + grid_size=(GRID_WIDTH, GRID_HEIGHT), texture=texture, + zoom=1.5 +) +create_map(grid) + +player = mcrfpy.Entity(grid_pos=(5, 10), texture=texture, sprite_index=SPRITE_PLAYER) +grid.entities.append(player) +scene.children.append(grid) + +title = mcrfpy.Caption(pos=(80, 20), text="Part 2: Walls and Collision") +title.fill_color = mcrfpy.Color(255, 255, 255) +title.font_size = 24 +scene.children.append(title) + +instructions = mcrfpy.Caption(pos=(80, 55), text="WASD or Arrow Keys | Walls block movement") +instructions.fill_color = mcrfpy.Color(180, 180, 180) +instructions.font_size = 16 +scene.children.append(instructions) + +pos_display = mcrfpy.Caption(pos=(80, 600), text=f"Position: ({player.grid_x}, {player.grid_y})") +pos_display.fill_color = mcrfpy.Color(200, 200, 100) +pos_display.font_size = 16 +scene.children.append(pos_display) + +status_display = mcrfpy.Caption(pos=(400, 600), text="Status: Ready") +status_display.fill_color = mcrfpy.Color(100, 200, 100) +status_display.font_size = 16 +scene.children.append(status_display) + +def handle_keys(key: mcrfpy.Key, action: mcrfpy.InputState) -> None: + if action != mcrfpy.InputState.PRESSED: + return + px, py = player.grid_x, player.grid_y + new_x, new_y = px, py + if key == mcrfpy.Key.W or key == mcrfpy.Key.UP: + new_y -= 1 + elif key == mcrfpy.Key.S or key == mcrfpy.Key.DOWN: + new_y += 1 + elif key == mcrfpy.Key.A or key == mcrfpy.Key.LEFT: + new_x -= 1 + elif key == mcrfpy.Key.D or key == mcrfpy.Key.RIGHT: + new_x += 1 + elif key == mcrfpy.Key.ESCAPE: + mcrfpy.exit() + return + else: + return + if can_move_to(grid, new_x, new_y): + player.grid_x = new_x + player.grid_y = new_y + pos_display.text = f"Position: ({new_x}, {new_y})" + status_display.text = "Status: Moved" + status_display.fill_color = mcrfpy.Color(100, 200, 100) + else: + status_display.text = "Status: Blocked!" + status_display.fill_color = mcrfpy.Color(200, 100, 100) + +scene.on_key = handle_keys +scene.activate() diff --git a/tests/snippets/273_part_03_dungeon_generation.py b/tests/snippets/273_part_03_dungeon_generation.py new file mode 100644 index 0000000..efafca4 --- /dev/null +++ b/tests/snippets/273_part_03_dungeon_generation.py @@ -0,0 +1,352 @@ +# mcrf: objects=[Caption,Color,Entity,Grid,InputState,Key,Scene,Texture] verified=0.2.8-dev status=ok +"""McRogueFace Tutorial - Part 3: Procedural Dungeon Generation + +Generate random dungeons with rooms and corridors. +""" +import mcrfpy +import random + +# ============================================================================= +# Constants +# ============================================================================= + +# Sprite indices for CP437 tileset +SPRITE_WALL = 35 # '#' - wall +SPRITE_FLOOR = 46 # '.' - floor +SPRITE_PLAYER = 64 # '@' - player + +# Grid dimensions +GRID_WIDTH = 50 +GRID_HEIGHT = 35 + +# Room generation parameters +ROOM_MIN_SIZE = 6 +ROOM_MAX_SIZE = 12 +MAX_ROOMS = 8 + +# ============================================================================= +# Room Class +# ============================================================================= + +class RectangularRoom: + """A rectangular room with its position and size.""" + + def __init__(self, x: int, y: int, width: int, height: int): + """Create a new room. + + Args: + x: Left edge X coordinate + y: Top edge Y coordinate + width: Room width in tiles + height: Room height in tiles + """ + self.x1 = x + self.y1 = y + self.x2 = x + width + self.y2 = y + height + + @property + def center(self) -> tuple[int, int]: + """Return the center coordinates of the room.""" + center_x = (self.x1 + self.x2) // 2 + center_y = (self.y1 + self.y2) // 2 + return center_x, center_y + + @property + def inner(self) -> tuple[slice, slice]: + """Return the inner area of the room (excluding walls). + + The inner area is one tile smaller on each side to leave room + for walls between adjacent rooms. + """ + return slice(self.x1 + 1, self.x2), slice(self.y1 + 1, self.y2) + + def intersects(self, other: "RectangularRoom") -> bool: + """Check if this room overlaps with another room. + + Args: + other: Another RectangularRoom to check against + + Returns: + True if the rooms overlap, False otherwise + """ + return ( + self.x1 <= other.x2 and + self.x2 >= other.x1 and + self.y1 <= other.y2 and + self.y2 >= other.y1 + ) + +# ============================================================================= +# Dungeon Generation +# ============================================================================= + +def fill_with_walls(grid: mcrfpy.Grid) -> None: + """Fill the entire grid with wall tiles.""" + for y in range(GRID_HEIGHT): + for x in range(GRID_WIDTH): + cell = grid.at(x, y) + cell.tilesprite = SPRITE_WALL + cell.walkable = False + cell.transparent = False + +def carve_room(grid: mcrfpy.Grid, room: RectangularRoom) -> None: + """Carve out a room by setting its inner tiles to floor. + + Args: + grid: The game grid + room: The room to carve + """ + inner_x, inner_y = room.inner + for y in range(inner_y.start, inner_y.stop): + for x in range(inner_x.start, inner_x.stop): + if 0 <= x < GRID_WIDTH and 0 <= y < GRID_HEIGHT: + cell = grid.at(x, y) + cell.tilesprite = SPRITE_FLOOR + cell.walkable = True + cell.transparent = True + +def carve_tunnel_horizontal(grid: mcrfpy.Grid, x1: int, x2: int, y: int) -> None: + """Carve a horizontal tunnel. + + Args: + grid: The game grid + x1: Starting X coordinate + x2: Ending X coordinate + y: Y coordinate of the tunnel + """ + start_x = min(x1, x2) + end_x = max(x1, x2) + for x in range(start_x, end_x + 1): + if 0 <= x < GRID_WIDTH and 0 <= y < GRID_HEIGHT: + cell = grid.at(x, y) + cell.tilesprite = SPRITE_FLOOR + cell.walkable = True + cell.transparent = True + +def carve_tunnel_vertical(grid: mcrfpy.Grid, y1: int, y2: int, x: int) -> None: + """Carve a vertical tunnel. + + Args: + grid: The game grid + y1: Starting Y coordinate + y2: Ending Y coordinate + x: X coordinate of the tunnel + """ + start_y = min(y1, y2) + end_y = max(y1, y2) + for y in range(start_y, end_y + 1): + if 0 <= x < GRID_WIDTH and 0 <= y < GRID_HEIGHT: + cell = grid.at(x, y) + cell.tilesprite = SPRITE_FLOOR + cell.walkable = True + cell.transparent = True + +def carve_l_tunnel( + grid: mcrfpy.Grid, + start: tuple[int, int], + end: tuple[int, int] +) -> None: + """Carve an L-shaped tunnel between two points. + + Randomly chooses to go horizontal-then-vertical or vertical-then-horizontal. + + Args: + grid: The game grid + start: Starting (x, y) coordinates + end: Ending (x, y) coordinates + """ + x1, y1 = start + x2, y2 = end + + # Randomly choose whether to go horizontal or vertical first + if random.random() < 0.5: + # Horizontal first, then vertical + carve_tunnel_horizontal(grid, x1, x2, y1) + carve_tunnel_vertical(grid, y1, y2, x2) + else: + # Vertical first, then horizontal + carve_tunnel_vertical(grid, y1, y2, x1) + carve_tunnel_horizontal(grid, x1, x2, y2) + +def generate_dungeon(grid: mcrfpy.Grid) -> tuple[int, int]: + """Generate a dungeon with rooms and tunnels. + + Args: + grid: The game grid to generate the dungeon in + + Returns: + The (x, y) coordinates where the player should start + """ + # Start with all walls + fill_with_walls(grid) + + rooms: list[RectangularRoom] = [] + + for _ in range(MAX_ROOMS): + # Random room dimensions + room_width = random.randint(ROOM_MIN_SIZE, ROOM_MAX_SIZE) + room_height = random.randint(ROOM_MIN_SIZE, ROOM_MAX_SIZE) + + # Random position (leaving 1-tile border) + x = random.randint(1, GRID_WIDTH - room_width - 2) + y = random.randint(1, GRID_HEIGHT - room_height - 2) + + new_room = RectangularRoom(x, y, room_width, room_height) + + # Check for overlap with existing rooms + overlaps = False + for other_room in rooms: + if new_room.intersects(other_room): + overlaps = True + break + + if overlaps: + continue # Skip this room, try another + + # No overlap - carve out the room + carve_room(grid, new_room) + + # Connect to previous room with a tunnel + if rooms: + # Tunnel from this room's center to the previous room's center + carve_l_tunnel(grid, new_room.center, rooms[-1].center) + + rooms.append(new_room) + + # Return the center of the first room as the player start position + if rooms: + return rooms[0].center + else: + # Fallback if no rooms were generated + return GRID_WIDTH // 2, GRID_HEIGHT // 2 + +# ============================================================================= +# Collision Detection +# ============================================================================= + +def can_move_to(grid: mcrfpy.Grid, x: int, y: int) -> bool: + """Check if a position is valid for movement.""" + if x < 0 or x >= GRID_WIDTH or y < 0 or y >= GRID_HEIGHT: + return False + return grid.at(x, y).walkable + +# ============================================================================= +# Game Setup +# ============================================================================= + +# Create the scene +scene = mcrfpy.Scene("game") + +# Load texture +texture = mcrfpy.Texture("assets/kenney_tinydungeon.png", 16, 16) + +# Create the grid +grid = mcrfpy.Grid( + pos=(50, 80), + size=(800, 560), + grid_size=(GRID_WIDTH, GRID_HEIGHT), + texture=texture, + zoom=1.0 +) + +# Generate the dungeon and get player start position +player_start_x, player_start_y = generate_dungeon(grid) + +# Create the player at the starting position +player = mcrfpy.Entity( + grid_pos=(player_start_x, player_start_y), + texture=texture, + sprite_index=SPRITE_PLAYER +) +grid.entities.append(player) + +# Add grid to scene +scene.children.append(grid) + +# ============================================================================= +# UI Elements +# ============================================================================= + +title = mcrfpy.Caption( + pos=(50, 15), + text="Part 3: Procedural Dungeon Generation" +) +title.fill_color = mcrfpy.Color(255, 255, 255) +title.font_size = 24 +scene.children.append(title) + +instructions = mcrfpy.Caption( + pos=(50, 50), + text="WASD/Arrows: Move | R: Regenerate dungeon | Escape: Quit" +) +instructions.fill_color = mcrfpy.Color(180, 180, 180) +instructions.font_size = 16 +scene.children.append(instructions) + +pos_display = mcrfpy.Caption( + pos=(50, 660), + text=f"Position: ({player.grid_x}, {player.grid_y})" +) +pos_display.fill_color = mcrfpy.Color(200, 200, 100) +pos_display.font_size = 16 +scene.children.append(pos_display) + +room_display = mcrfpy.Caption( + pos=(400, 660), + text="Press R to regenerate the dungeon" +) +room_display.fill_color = mcrfpy.Color(100, 200, 100) +room_display.font_size = 16 +scene.children.append(room_display) + +# ============================================================================= +# Input Handling +# ============================================================================= + +def regenerate_dungeon() -> None: + """Generate a new dungeon and reposition the player.""" + new_x, new_y = generate_dungeon(grid) + player.grid_x = new_x + player.grid_y = new_y + pos_display.text = f"Position: ({new_x}, {new_y})" + room_display.text = "New dungeon generated!" + +def handle_keys(key: mcrfpy.Key, action: mcrfpy.InputState) -> None: + """Handle keyboard input.""" + if action != mcrfpy.InputState.PRESSED: + return + + px, py = player.grid_x, player.grid_y + new_x, new_y = px, py + + if key == mcrfpy.Key.W or key == mcrfpy.Key.UP: + new_y -= 1 + elif key == mcrfpy.Key.S or key == mcrfpy.Key.DOWN: + new_y += 1 + elif key == mcrfpy.Key.A or key == mcrfpy.Key.LEFT: + new_x -= 1 + elif key == mcrfpy.Key.D or key == mcrfpy.Key.RIGHT: + new_x += 1 + elif key == mcrfpy.Key.R: + regenerate_dungeon() + return + elif key == mcrfpy.Key.ESCAPE: + mcrfpy.exit() + return + else: + return + + if can_move_to(grid, new_x, new_y): + player.grid_x = new_x + player.grid_y = new_y + pos_display.text = f"Position: ({new_x}, {new_y})" + +scene.on_key = handle_keys + +# ============================================================================= +# Start the Game +# ============================================================================= + +scene.activate() +print("Part 3 loaded! Explore the dungeon or press R to regenerate.") diff --git a/tests/snippets/274_part_04_fov.py b/tests/snippets/274_part_04_fov.py new file mode 100644 index 0000000..275afde --- /dev/null +++ b/tests/snippets/274_part_04_fov.py @@ -0,0 +1,367 @@ +# mcrf: objects=[Caption,Color,ColorLayer,Entity,FOV,Grid,InputState,Key,Scene,Texture] verified=0.2.8-dev status=ok +"""McRogueFace Tutorial - Part 4: Field of View + +Implement fog of war with visible, explored, and unknown tiles. +""" +import mcrfpy +import random + +# ============================================================================= +# Constants +# ============================================================================= + +# Sprite indices for CP437 tileset +SPRITE_WALL = 35 # '#' - wall +SPRITE_FLOOR = 46 # '.' - floor +SPRITE_PLAYER = 64 # '@' - player + +# Grid dimensions +GRID_WIDTH = 50 +GRID_HEIGHT = 35 + +# Room generation parameters +ROOM_MIN_SIZE = 6 +ROOM_MAX_SIZE = 12 +MAX_ROOMS = 8 + +# FOV settings +FOV_RADIUS = 8 + +# Visibility colors (applied as overlays) +COLOR_VISIBLE = mcrfpy.Color(0, 0, 0, 0) # Fully transparent - show tile +COLOR_DISCOVERED = mcrfpy.Color(0, 0, 40, 180) # Dark blue tint - dimmed +COLOR_UNKNOWN = mcrfpy.Color(0, 0, 0, 255) # Solid black - hidden + +# ============================================================================= +# Room Class (from Part 3) +# ============================================================================= + +class RectangularRoom: + """A rectangular room with its position and size.""" + + def __init__(self, x: int, y: int, width: int, height: int): + self.x1 = x + self.y1 = y + self.x2 = x + width + self.y2 = y + height + + @property + def center(self) -> tuple[int, int]: + center_x = (self.x1 + self.x2) // 2 + center_y = (self.y1 + self.y2) // 2 + return center_x, center_y + + @property + def inner(self) -> tuple[slice, slice]: + return slice(self.x1 + 1, self.x2), slice(self.y1 + 1, self.y2) + + def intersects(self, other: "RectangularRoom") -> bool: + return ( + self.x1 <= other.x2 and + self.x2 >= other.x1 and + self.y1 <= other.y2 and + self.y2 >= other.y1 + ) + +# ============================================================================= +# Exploration Tracking +# ============================================================================= + +# Track which tiles have been discovered (seen at least once) +explored: list[list[bool]] = [] + +def init_explored() -> None: + """Initialize the explored array to all False.""" + global explored + explored = [[False for _ in range(GRID_WIDTH)] for _ in range(GRID_HEIGHT)] + +def mark_explored(x: int, y: int) -> None: + """Mark a tile as explored.""" + if 0 <= x < GRID_WIDTH and 0 <= y < GRID_HEIGHT: + explored[y][x] = True + +def is_explored(x: int, y: int) -> bool: + """Check if a tile has been explored.""" + if 0 <= x < GRID_WIDTH and 0 <= y < GRID_HEIGHT: + return explored[y][x] + return False + +# ============================================================================= +# Dungeon Generation (from Part 3, with transparent property) +# ============================================================================= + +def fill_with_walls(grid: mcrfpy.Grid) -> None: + """Fill the entire grid with wall tiles.""" + for y in range(GRID_HEIGHT): + for x in range(GRID_WIDTH): + cell = grid.at(x, y) + cell.tilesprite = SPRITE_WALL + cell.walkable = False + cell.transparent = False # Walls block line of sight + +def carve_room(grid: mcrfpy.Grid, room: RectangularRoom) -> None: + """Carve out a room by setting its inner tiles to floor.""" + inner_x, inner_y = room.inner + for y in range(inner_y.start, inner_y.stop): + for x in range(inner_x.start, inner_x.stop): + if 0 <= x < GRID_WIDTH and 0 <= y < GRID_HEIGHT: + cell = grid.at(x, y) + cell.tilesprite = SPRITE_FLOOR + cell.walkable = True + cell.transparent = True # Floors allow line of sight + +def carve_tunnel_horizontal(grid: mcrfpy.Grid, x1: int, x2: int, y: int) -> None: + """Carve a horizontal tunnel.""" + for x in range(min(x1, x2), max(x1, x2) + 1): + if 0 <= x < GRID_WIDTH and 0 <= y < GRID_HEIGHT: + cell = grid.at(x, y) + cell.tilesprite = SPRITE_FLOOR + cell.walkable = True + cell.transparent = True + +def carve_tunnel_vertical(grid: mcrfpy.Grid, y1: int, y2: int, x: int) -> None: + """Carve a vertical tunnel.""" + for y in range(min(y1, y2), max(y1, y2) + 1): + if 0 <= x < GRID_WIDTH and 0 <= y < GRID_HEIGHT: + cell = grid.at(x, y) + cell.tilesprite = SPRITE_FLOOR + cell.walkable = True + cell.transparent = True + +def carve_l_tunnel( + grid: mcrfpy.Grid, + start: tuple[int, int], + end: tuple[int, int] +) -> None: + """Carve an L-shaped tunnel between two points.""" + x1, y1 = start + x2, y2 = end + + if random.random() < 0.5: + carve_tunnel_horizontal(grid, x1, x2, y1) + carve_tunnel_vertical(grid, y1, y2, x2) + else: + carve_tunnel_vertical(grid, y1, y2, x1) + carve_tunnel_horizontal(grid, x1, x2, y2) + +def generate_dungeon(grid: mcrfpy.Grid) -> tuple[int, int]: + """Generate a dungeon with rooms and tunnels.""" + fill_with_walls(grid) + init_explored() # Reset exploration when generating new dungeon + + rooms: list[RectangularRoom] = [] + + for _ in range(MAX_ROOMS): + room_width = random.randint(ROOM_MIN_SIZE, ROOM_MAX_SIZE) + room_height = random.randint(ROOM_MIN_SIZE, ROOM_MAX_SIZE) + x = random.randint(1, GRID_WIDTH - room_width - 2) + y = random.randint(1, GRID_HEIGHT - room_height - 2) + + new_room = RectangularRoom(x, y, room_width, room_height) + + overlaps = False + for other_room in rooms: + if new_room.intersects(other_room): + overlaps = True + break + + if overlaps: + continue + + carve_room(grid, new_room) + if rooms: + carve_l_tunnel(grid, new_room.center, rooms[-1].center) + rooms.append(new_room) + + if rooms: + return rooms[0].center + return GRID_WIDTH // 2, GRID_HEIGHT // 2 + +# ============================================================================= +# Field of View +# ============================================================================= + +def update_fov( + grid: mcrfpy.Grid, + fov_layer, + player_x: int, + player_y: int, + radius: int = FOV_RADIUS, +) -> None: + """Update the field of view visualization. + + Args: + grid: The game grid + fov_layer: The ColorLayer for FOV visualization + player_x: Player's X position + player_y: Player's Y position + radius: How far the player can see (defaults to FOV_RADIUS) + """ + # Compute FOV from player position + grid.compute_fov((player_x, player_y), radius=radius, algorithm=mcrfpy.FOV.SHADOW) + + # Update each tile's visibility + for y in range(GRID_HEIGHT): + for x in range(GRID_WIDTH): + if grid.is_in_fov(x, y): + # Currently visible - mark as explored and show clearly + mark_explored(x, y) + fov_layer.set((x, y), COLOR_VISIBLE) + elif is_explored(x, y): + # Previously seen but not currently visible - show dimmed + fov_layer.set((x, y), COLOR_DISCOVERED) + else: + # Never seen - hide completely + fov_layer.set((x, y), COLOR_UNKNOWN) + +# ============================================================================= +# Collision Detection +# ============================================================================= + +def can_move_to(grid: mcrfpy.Grid, x: int, y: int) -> bool: + """Check if a position is valid for movement.""" + if x < 0 or x >= GRID_WIDTH or y < 0 or y >= GRID_HEIGHT: + return False + return grid.at(x, y).walkable + +# ============================================================================= +# Game Setup +# ============================================================================= + +# Create the scene +scene = mcrfpy.Scene("game") + +# Load texture +texture = mcrfpy.Texture("assets/kenney_tinydungeon.png", 16, 16) + +# Create the grid +grid = mcrfpy.Grid( + pos=(50, 80), + size=(800, 560), + grid_size=(GRID_WIDTH, GRID_HEIGHT), + texture=texture, + zoom=1.0 +) + +# Generate the dungeon +player_start_x, player_start_y = generate_dungeon(grid) + +# Add a color layer for FOV visualization (below entities) +fov_layer = mcrfpy.ColorLayer(name="fov", z_index=-1) +grid.add_layer(fov_layer) + +# Initialize the FOV layer to all black (unknown) +for y in range(GRID_HEIGHT): + for x in range(GRID_WIDTH): + fov_layer.set((x, y), COLOR_UNKNOWN) + +# Create the player +player = mcrfpy.Entity( + grid_pos=(player_start_x, player_start_y), + texture=texture, + sprite_index=SPRITE_PLAYER +) +grid.entities.append(player) + +# Calculate initial FOV +update_fov(grid, fov_layer, player_start_x, player_start_y) + +# Add grid to scene +scene.children.append(grid) + +# ============================================================================= +# UI Elements +# ============================================================================= + +title = mcrfpy.Caption( + pos=(50, 15), + text="Part 4: Field of View" +) +title.fill_color = mcrfpy.Color(255, 255, 255) +title.font_size = 24 +scene.children.append(title) + +instructions = mcrfpy.Caption( + pos=(50, 50), + text="WASD/Arrows: Move | R: Regenerate | Escape: Quit" +) +instructions.fill_color = mcrfpy.Color(180, 180, 180) +instructions.font_size = 16 +scene.children.append(instructions) + +pos_display = mcrfpy.Caption( + pos=(50, 660), + text=f"Position: ({player.grid_x}, {player.grid_y})" +) +pos_display.fill_color = mcrfpy.Color(200, 200, 100) +pos_display.font_size = 16 +scene.children.append(pos_display) + +fov_display = mcrfpy.Caption( + pos=(400, 660), + text=f"FOV Radius: {FOV_RADIUS}" +) +fov_display.fill_color = mcrfpy.Color(100, 200, 100) +fov_display.font_size = 16 +scene.children.append(fov_display) + +# ============================================================================= +# Input Handling +# ============================================================================= + +def regenerate_dungeon() -> None: + """Generate a new dungeon and reposition the player.""" + new_x, new_y = generate_dungeon(grid) + player.grid_x = new_x + player.grid_y = new_y + + # Reset FOV layer to unknown + for y in range(GRID_HEIGHT): + for x in range(GRID_WIDTH): + fov_layer.set((x, y), COLOR_UNKNOWN) + + # Calculate new FOV + update_fov(grid, fov_layer, new_x, new_y) + pos_display.text = f"Position: ({new_x}, {new_y})" + +def handle_keys(key: mcrfpy.Key, action: mcrfpy.InputState) -> None: + """Handle keyboard input.""" + if action != mcrfpy.InputState.PRESSED: + return + + px, py = player.grid_x, player.grid_y + new_x, new_y = px, py + + if key == mcrfpy.Key.W or key == mcrfpy.Key.Up: + new_y -= 1 + elif key == mcrfpy.Key.S or key == mcrfpy.Key.Down: + new_y += 1 + elif key == mcrfpy.Key.A or key == mcrfpy.Key.Left: + new_x -= 1 + elif key == mcrfpy.Key.D or key == mcrfpy.Key.Right: + new_x += 1 + elif key == mcrfpy.Key.R: + regenerate_dungeon() + return + elif key == mcrfpy.Key.Escape: + mcrfpy.exit() + return + else: + return + + if can_move_to(grid, new_x, new_y): + player.grid_x = new_x + player.grid_y = new_y + pos_display.text = f"Position: ({new_x}, {new_y})" + + # Update FOV after movement + update_fov(grid, fov_layer, new_x, new_y) + +scene.on_key = handle_keys + +# ============================================================================= +# Start the Game +# ============================================================================= + +scene.activate() +print("Part 4 loaded! Explore the dungeon - watch the fog of war!") diff --git a/tests/snippets/275_part_05_enemies.py b/tests/snippets/275_part_05_enemies.py new file mode 100644 index 0000000..c8da4bd --- /dev/null +++ b/tests/snippets/275_part_05_enemies.py @@ -0,0 +1,673 @@ +# mcrf: objects=[Caption,Color,ColorLayer,Entity,FOV,Grid,InputState,Key,Scene,Texture] verified=0.2.8-dev status=ok +"""McRogueFace Tutorial - Part 5: Placing Enemies + +Add enemies to the dungeon that lurk in the shadows. +""" +import mcrfpy +import random + +# ============================================================================= +# Constants +# ============================================================================= + +# Sprite indices for CP437 tileset +SPRITE_WALL = 35 # '#' - wall +SPRITE_FLOOR = 46 # '.' - floor +SPRITE_PLAYER = 64 # '@' - player + +# Enemy sprites (lowercase letters in CP437) +SPRITE_GOBLIN = 103 # 'g' +SPRITE_ORC = 111 # 'o' +SPRITE_TROLL = 116 # 't' + +# Grid dimensions +GRID_WIDTH = 50 +GRID_HEIGHT = 35 + +# Room generation parameters +ROOM_MIN_SIZE = 6 +ROOM_MAX_SIZE = 12 +MAX_ROOMS = 8 + +# Enemy spawn parameters +MAX_ENEMIES_PER_ROOM = 3 + +# FOV settings +FOV_RADIUS = 8 + +# Visibility colors +COLOR_VISIBLE = mcrfpy.Color(0, 0, 0, 0) +COLOR_DISCOVERED = mcrfpy.Color(0, 0, 40, 180) +COLOR_UNKNOWN = mcrfpy.Color(0, 0, 0, 255) + +# ============================================================================= +# Enemy Data +# ============================================================================= + +# Enemy templates - stats for each enemy type +ENEMY_TEMPLATES = { + "goblin": { + "sprite": SPRITE_GOBLIN, + "hp": 6, + "max_hp": 6, + "attack": 3, + "defense": 0 # Greenish + }, + "orc": { + "sprite": SPRITE_ORC, + "hp": 10, + "max_hp": 10, + "attack": 4, + "defense": 1 # Darker green + }, + "troll": { + "sprite": SPRITE_TROLL, + "hp": 16, + "max_hp": 16, + "attack": 6, + "defense": 2 # Dark green + } +} + +# Global storage for entity data +# Maps entity objects to their data dictionaries +entity_data: dict = {} + +# Global references +player = None +grid = None +fov_layer = None + +# ============================================================================= +# Room Class (from Part 3) +# ============================================================================= + +class RectangularRoom: + """A rectangular room with its position and size.""" + + def __init__(self, x: int, y: int, width: int, height: int): + self.x1 = x + self.y1 = y + self.x2 = x + width + self.y2 = y + height + + @property + def center(self) -> tuple[int, int]: + center_x = (self.x1 + self.x2) // 2 + center_y = (self.y1 + self.y2) // 2 + return center_x, center_y + + @property + def inner(self) -> tuple[slice, slice]: + return slice(self.x1 + 1, self.x2), slice(self.y1 + 1, self.y2) + + def intersects(self, other: "RectangularRoom") -> bool: + return ( + self.x1 <= other.x2 and + self.x2 >= other.x1 and + self.y1 <= other.y2 and + self.y2 >= other.y1 + ) + +# ============================================================================= +# Exploration Tracking (from Part 4) +# ============================================================================= + +explored: list[list[bool]] = [] + +def init_explored() -> None: + """Initialize the explored array to all False.""" + global explored + explored = [[False for _ in range(GRID_WIDTH)] for _ in range(GRID_HEIGHT)] + +def mark_explored(x: int, y: int) -> None: + """Mark a tile as explored.""" + if 0 <= x < GRID_WIDTH and 0 <= y < GRID_HEIGHT: + explored[y][x] = True + +def is_explored(x: int, y: int) -> bool: + """Check if a tile has been explored.""" + if 0 <= x < GRID_WIDTH and 0 <= y < GRID_HEIGHT: + return explored[y][x] + return False + +# ============================================================================= +# Dungeon Generation (from Part 4) +# ============================================================================= + +def fill_with_walls(target_grid: mcrfpy.Grid) -> None: + """Fill the entire grid with wall tiles.""" + for y in range(GRID_HEIGHT): + for x in range(GRID_WIDTH): + cell = target_grid.at(x, y) + cell.tilesprite = SPRITE_WALL + cell.walkable = False + cell.transparent = False + +def carve_room(target_grid: mcrfpy.Grid, room: RectangularRoom) -> None: + """Carve out a room by setting its inner tiles to floor.""" + inner_x, inner_y = room.inner + for y in range(inner_y.start, inner_y.stop): + for x in range(inner_x.start, inner_x.stop): + if 0 <= x < GRID_WIDTH and 0 <= y < GRID_HEIGHT: + cell = target_grid.at(x, y) + cell.tilesprite = SPRITE_FLOOR + cell.walkable = True + cell.transparent = True + +def carve_tunnel_horizontal(target_grid: mcrfpy.Grid, x1: int, x2: int, y: int) -> None: + """Carve a horizontal tunnel.""" + for x in range(min(x1, x2), max(x1, x2) + 1): + if 0 <= x < GRID_WIDTH and 0 <= y < GRID_HEIGHT: + cell = target_grid.at(x, y) + cell.tilesprite = SPRITE_FLOOR + cell.walkable = True + cell.transparent = True + +def carve_tunnel_vertical(target_grid: mcrfpy.Grid, y1: int, y2: int, x: int) -> None: + """Carve a vertical tunnel.""" + for y in range(min(y1, y2), max(y1, y2) + 1): + if 0 <= x < GRID_WIDTH and 0 <= y < GRID_HEIGHT: + cell = target_grid.at(x, y) + cell.tilesprite = SPRITE_FLOOR + cell.walkable = True + cell.transparent = True + +def carve_l_tunnel( + target_grid: mcrfpy.Grid, + start: tuple[int, int], + end: tuple[int, int] +) -> None: + """Carve an L-shaped tunnel between two points.""" + x1, y1 = start + x2, y2 = end + + if random.random() < 0.5: + carve_tunnel_horizontal(target_grid, x1, x2, y1) + carve_tunnel_vertical(target_grid, y1, y2, x2) + else: + carve_tunnel_vertical(target_grid, y1, y2, x1) + carve_tunnel_horizontal(target_grid, x1, x2, y2) + +# ============================================================================= +# Enemy Management +# ============================================================================= + +def spawn_enemy(target_grid: mcrfpy.Grid, x: int, y: int, enemy_type: str, texture: mcrfpy.Texture) -> mcrfpy.Entity: + """Spawn an enemy at the given position. + + Args: + target_grid: The game grid + x: X position in tiles + y: Y position in tiles + enemy_type: Type of enemy ("goblin", "orc", or "troll") + texture: The texture to use for the sprite + + Returns: + The created enemy Entity + """ + template = ENEMY_TEMPLATES[enemy_type] + + enemy = mcrfpy.Entity( + grid_pos=(x, y), + texture=texture, + sprite_index=template["sprite"] + ) +# Start hidden until player sees them + enemy.visible = False + + # Add to grid + target_grid.entities.append(enemy) + + # Store enemy data + entity_data[enemy] = { + "type": enemy_type, + "name": enemy_type.capitalize(), + "hp": template["hp"], + "max_hp": template["max_hp"], + "attack": template["attack"], + "defense": template["defense"], + "is_player": False + } + + return enemy + +def spawn_enemies_in_room(target_grid: mcrfpy.Grid, room: RectangularRoom, texture: mcrfpy.Texture) -> None: + """Spawn random enemies in a room. + + Args: + target_grid: The game grid + room: The room to spawn enemies in + texture: The texture to use for sprites + """ + # Random number of enemies (0 to MAX_ENEMIES_PER_ROOM) + num_enemies = random.randint(0, MAX_ENEMIES_PER_ROOM) + + for _ in range(num_enemies): + # Random position within the room's inner area + inner_x, inner_y = room.inner + x = random.randint(inner_x.start, inner_x.stop - 1) + y = random.randint(inner_y.start, inner_y.stop - 1) + + # Check if position is already occupied + if get_blocking_entity_at(target_grid, x, y) is not None: + continue # Skip this spawn attempt + + # Choose enemy type based on weighted random + roll = random.random() + if roll < 0.6: + enemy_type = "goblin" # 60% chance + elif roll < 0.9: + enemy_type = "orc" # 30% chance + else: + enemy_type = "troll" # 10% chance + + spawn_enemy(target_grid, x, y, enemy_type, texture) + +def get_blocking_entity_at(target_grid: mcrfpy.Grid, x: int, y: int) -> mcrfpy.Entity | None: + """Get any entity that blocks movement at the given position. + + Args: + target_grid: The game grid + x: X position to check + y: Y position to check + + Returns: + The blocking entity, or None if no entity blocks this position + """ + for entity in target_grid.entities: + if entity.grid_x == x and entity.grid_y == y: + return entity + return None + +def clear_enemies(target_grid: mcrfpy.Grid) -> None: + """Remove all enemies from the grid.""" + global entity_data + + # Get list of enemies to remove (not the player) + enemies_to_remove = [] + for entity in target_grid.entities: + if entity in entity_data and not entity_data[entity].get("is_player", False): + enemies_to_remove.append(entity) + + # Remove from grid and entity_data + for enemy in enemies_to_remove: + target_grid.entities.remove(enemy) + # Remove from entity_data + if enemy in entity_data: + del entity_data[enemy] + +# ============================================================================= +# Entity Visibility +# ============================================================================= + +def update_entity_visibility(target_grid: mcrfpy.Grid) -> None: + """Update visibility of all entities based on FOV. + + Entities outside the player's field of view are hidden. + """ + global player + + for entity in target_grid.entities: + # Player is always visible + if entity == player: + entity.visible = True + continue + + # Other entities are only visible if in FOV + ex, ey = entity.grid_x, entity.grid_y + entity.visible = target_grid.is_in_fov(ex, ey) + +# ============================================================================= +# Field of View (from Part 4) +# ============================================================================= + +def update_fov(target_grid: mcrfpy.Grid, target_fov_layer, player_x: int, player_y: int) -> None: + """Update the field of view visualization.""" + # Compute FOV from player position + target_grid.compute_fov((player_x, player_y), radius=FOV_RADIUS, algorithm=mcrfpy.FOV.SHADOW) + + # Update each tile's visibility + for y in range(GRID_HEIGHT): + for x in range(GRID_WIDTH): + if target_grid.is_in_fov(x, y): + mark_explored(x, y) + target_fov_layer.set((x, y), COLOR_VISIBLE) + elif is_explored(x, y): + target_fov_layer.set((x, y), COLOR_DISCOVERED) + else: + target_fov_layer.set((x, y), COLOR_UNKNOWN) + + # Update entity visibility + update_entity_visibility(target_grid) + +# ============================================================================= +# Collision Detection +# ============================================================================= + +def can_move_to(target_grid: mcrfpy.Grid, x: int, y: int) -> bool: + """Check if a position is valid for movement. + + A position is valid if: + 1. It is within grid bounds + 2. The tile is walkable + 3. No entity is blocking it + """ + # Check bounds + if x < 0 or x >= GRID_WIDTH or y < 0 or y >= GRID_HEIGHT: + return False + + # Check tile walkability + if not target_grid.at(x, y).walkable: + return False + + # Check for blocking entities + if get_blocking_entity_at(target_grid, x, y) is not None: + return False + + return True + +# ============================================================================= +# Dungeon Generation with Enemies +# ============================================================================= + +def generate_dungeon(target_grid: mcrfpy.Grid, texture: mcrfpy.Texture) -> tuple[int, int]: + """Generate a dungeon with rooms, tunnels, and enemies. + + Args: + target_grid: The game grid + texture: The texture for entity sprites + + Returns: + The (x, y) coordinates where the player should start + """ + # Clear any existing enemies + clear_enemies(target_grid) + + # Fill with walls + fill_with_walls(target_grid) + init_explored() + + rooms: list[RectangularRoom] = [] + + for _ in range(MAX_ROOMS): + room_width = random.randint(ROOM_MIN_SIZE, ROOM_MAX_SIZE) + room_height = random.randint(ROOM_MIN_SIZE, ROOM_MAX_SIZE) + x = random.randint(1, GRID_WIDTH - room_width - 2) + y = random.randint(1, GRID_HEIGHT - room_height - 2) + + new_room = RectangularRoom(x, y, room_width, room_height) + + overlaps = False + for other_room in rooms: + if new_room.intersects(other_room): + overlaps = True + break + + if overlaps: + continue + + carve_room(target_grid, new_room) + + if rooms: + carve_l_tunnel(target_grid, new_room.center, rooms[-1].center) + # Spawn enemies in all rooms except the first (player starting room) + spawn_enemies_in_room(target_grid, new_room, texture) + + rooms.append(new_room) + + if rooms: + return rooms[0].center + return GRID_WIDTH // 2, GRID_HEIGHT // 2 + +# ============================================================================= +# Game Setup +# ============================================================================= + +# Create the scene +scene = mcrfpy.Scene("game") + +# Load texture +texture = mcrfpy.Texture("assets/kenney_tinydungeon.png", 16, 16) + +# Create the grid +grid = mcrfpy.Grid( + pos=(50, 80), + size=(800, 560), + grid_size=(GRID_WIDTH, GRID_HEIGHT), + texture=texture, + zoom=1.0 +) + +# Generate the dungeon (without player first to get starting position) +fill_with_walls(grid) +init_explored() + +rooms: list[RectangularRoom] = [] + +for _ in range(MAX_ROOMS): + room_width = random.randint(ROOM_MIN_SIZE, ROOM_MAX_SIZE) + room_height = random.randint(ROOM_MIN_SIZE, ROOM_MAX_SIZE) + x = random.randint(1, GRID_WIDTH - room_width - 2) + y = random.randint(1, GRID_HEIGHT - room_height - 2) + + new_room = RectangularRoom(x, y, room_width, room_height) + + overlaps = False + for other_room in rooms: + if new_room.intersects(other_room): + overlaps = True + break + + if overlaps: + continue + + carve_room(grid, new_room) + + if rooms: + carve_l_tunnel(grid, new_room.center, rooms[-1].center) + + rooms.append(new_room) + +# Get player starting position +if rooms: + player_start_x, player_start_y = rooms[0].center +else: + player_start_x, player_start_y = GRID_WIDTH // 2, GRID_HEIGHT // 2 + +# Add FOV layer +fov_layer = mcrfpy.ColorLayer(name="fov", z_index=-1) +grid.add_layer(fov_layer) +for y in range(GRID_HEIGHT): + for x in range(GRID_WIDTH): + fov_layer.set((x, y), COLOR_UNKNOWN) + +# Create the player +player = mcrfpy.Entity( + grid_pos=(player_start_x, player_start_y), + texture=texture, + sprite_index=SPRITE_PLAYER +) +grid.entities.append(player) + +# Store player data +entity_data[player] = { + "type": "player", + "name": "Player", + "hp": 30, + "max_hp": 30, + "attack": 5, + "defense": 2, + "is_player": True +} + +# Now spawn enemies in rooms (except the first one) +for i, room in enumerate(rooms): + if i == 0: + continue # Skip player's starting room + spawn_enemies_in_room(grid, room, texture) + +# Calculate initial FOV +update_fov(grid, fov_layer, player_start_x, player_start_y) + +# Add grid to scene +scene.children.append(grid) + +# ============================================================================= +# UI Elements +# ============================================================================= + +title = mcrfpy.Caption( + pos=(50, 15), + text="Part 5: Placing Enemies" +) +title.fill_color = mcrfpy.Color(255, 255, 255) +title.font_size = 24 +scene.children.append(title) + +instructions = mcrfpy.Caption( + pos=(50, 50), + text="WASD/Arrows: Move | R: Regenerate | Escape: Quit" +) +instructions.fill_color = mcrfpy.Color(180, 180, 180) +instructions.font_size = 16 +scene.children.append(instructions) + +pos_display = mcrfpy.Caption( + pos=(50, 660), + text=f"Position: ({player.grid_x}, {player.grid_y})" +) +pos_display.fill_color = mcrfpy.Color(200, 200, 100) +pos_display.font_size = 16 +scene.children.append(pos_display) + +status_display = mcrfpy.Caption( + pos=(400, 660), + text="Explore the dungeon..." +) +status_display.fill_color = mcrfpy.Color(100, 200, 100) +status_display.font_size = 16 +scene.children.append(status_display) + +# ============================================================================= +# Input Handling +# ============================================================================= + +def regenerate_dungeon() -> None: + """Generate a new dungeon and reposition the player.""" + global player, grid, fov_layer, rooms + + # Clear enemies + clear_enemies(grid) + + # Regenerate dungeon structure + fill_with_walls(grid) + init_explored() + + rooms = [] + + for _ in range(MAX_ROOMS): + room_width = random.randint(ROOM_MIN_SIZE, ROOM_MAX_SIZE) + room_height = random.randint(ROOM_MIN_SIZE, ROOM_MAX_SIZE) + x = random.randint(1, GRID_WIDTH - room_width - 2) + y = random.randint(1, GRID_HEIGHT - room_height - 2) + + new_room = RectangularRoom(x, y, room_width, room_height) + + overlaps = False + for other_room in rooms: + if new_room.intersects(other_room): + overlaps = True + break + + if overlaps: + continue + + carve_room(grid, new_room) + + if rooms: + carve_l_tunnel(grid, new_room.center, rooms[-1].center) + + rooms.append(new_room) + + # Reposition player + if rooms: + new_x, new_y = rooms[0].center + else: + new_x, new_y = GRID_WIDTH // 2, GRID_HEIGHT // 2 + + player.grid_x = new_x + player.grid_y = new_y + + # Spawn new enemies + for i, room in enumerate(rooms): + if i == 0: + continue + spawn_enemies_in_room(grid, room, texture) + + # Reset FOV layer + for y in range(GRID_HEIGHT): + for x in range(GRID_WIDTH): + fov_layer.set((x, y), COLOR_UNKNOWN) + + # Update FOV + update_fov(grid, fov_layer, new_x, new_y) + pos_display.text = f"Position: ({new_x}, {new_y})" + status_display.text = "New dungeon generated!" + +def handle_keys(key: mcrfpy.Key, action: mcrfpy.InputState) -> None: + """Handle keyboard input.""" + global player, grid, fov_layer + + if action != mcrfpy.InputState.PRESSED: + return + + px, py = player.grid_x, player.grid_y + new_x, new_y = px, py + + if key == mcrfpy.Key.W or key == mcrfpy.Key.Up: + new_y -= 1 + elif key == mcrfpy.Key.S or key == mcrfpy.Key.Down: + new_y += 1 + elif key == mcrfpy.Key.A or key == mcrfpy.Key.Left: + new_x -= 1 + elif key == mcrfpy.Key.D or key == mcrfpy.Key.Right: + new_x += 1 + elif key == mcrfpy.Key.R: + regenerate_dungeon() + return + elif key == mcrfpy.Key.Escape: + mcrfpy.exit() + return + else: + return + + # Check for blocking entity (potential combat target) + blocker = get_blocking_entity_at(grid, new_x, new_y) + if blocker is not None and blocker != player: + # For now, just report that we bumped into an enemy + if blocker in entity_data: + enemy_name = entity_data[blocker]["name"] + status_display.text = f"A {enemy_name} blocks your path!" + status_display.fill_color = mcrfpy.Color(200, 150, 100) + return + + # Check if we can move + if can_move_to(grid, new_x, new_y): + player.grid_x = new_x + player.grid_y = new_y + pos_display.text = f"Position: ({new_x}, {new_y})" + status_display.text = "Exploring..." + status_display.fill_color = mcrfpy.Color(100, 200, 100) + + # Update FOV after movement + update_fov(grid, fov_layer, new_x, new_y) + +scene.on_key = handle_keys + +# ============================================================================= +# Start the Game +# ============================================================================= + +scene.activate() +print("Part 5 loaded! Enemies lurk in the dungeon...") diff --git a/tests/snippets/276_part_06_combat.py b/tests/snippets/276_part_06_combat.py new file mode 100644 index 0000000..3884c7e --- /dev/null +++ b/tests/snippets/276_part_06_combat.py @@ -0,0 +1,930 @@ +# mcrf: objects=[Caption,Color,ColorLayer,Entity,FOV,Grid,InputState,Key,Scene,Texture] verified=0.2.8-dev status=ok +"""McRogueFace Tutorial - Part 6: Combat System + +Implement bump-to-attack combat with HP, damage, and death. +""" +import mcrfpy +import random +from dataclasses import dataclass +from typing import Optional + +# ============================================================================= +# Constants +# ============================================================================= + +# Sprite indices for CP437 tileset +SPRITE_WALL = 35 # '#' - wall +SPRITE_FLOOR = 46 # '.' - floor +SPRITE_PLAYER = 64 # '@' - player +SPRITE_CORPSE = 37 # '%' - remains + +# Enemy sprites +SPRITE_GOBLIN = 103 # 'g' +SPRITE_ORC = 111 # 'o' +SPRITE_TROLL = 116 # 't' + +# Grid dimensions +GRID_WIDTH = 50 +GRID_HEIGHT = 35 + +# Room generation parameters +ROOM_MIN_SIZE = 6 +ROOM_MAX_SIZE = 12 +MAX_ROOMS = 8 + +# Enemy spawn parameters +MAX_ENEMIES_PER_ROOM = 3 + +# FOV settings +FOV_RADIUS = 8 + +# Visibility colors +COLOR_VISIBLE = mcrfpy.Color(0, 0, 0, 0) +COLOR_DISCOVERED = mcrfpy.Color(0, 0, 40, 180) +COLOR_UNKNOWN = mcrfpy.Color(0, 0, 0, 255) + +# Message log settings +MAX_MESSAGES = 5 + +# ============================================================================= +# Fighter Component +# ============================================================================= + +@dataclass +class Fighter: + """Combat stats for an entity.""" + hp: int + max_hp: int + attack: int + defense: int + name: str + is_player: bool = False + + @property + def is_alive(self) -> bool: + """Check if this fighter is still alive.""" + return self.hp > 0 + + def take_damage(self, amount: int) -> int: + """Apply damage and return actual damage taken.""" + actual_damage = min(self.hp, amount) + self.hp -= actual_damage + return actual_damage + + def heal(self, amount: int) -> int: + """Heal and return actual amount healed.""" + actual_heal = min(self.max_hp - self.hp, amount) + self.hp += actual_heal + return actual_heal + +# ============================================================================= +# Enemy Templates +# ============================================================================= + +ENEMY_TEMPLATES = { + "goblin": { + "sprite": SPRITE_GOBLIN, + "hp": 6, + "attack": 3, + "defense": 0 + }, + "orc": { + "sprite": SPRITE_ORC, + "hp": 10, + "attack": 4, + "defense": 1 + }, + "troll": { + "sprite": SPRITE_TROLL, + "hp": 16, + "attack": 6, + "defense": 2 + } +} + +# ============================================================================= +# Global State +# ============================================================================= + +# Entity data storage +entity_data: dict[mcrfpy.Entity, Fighter] = {} + +# Global references +player: Optional[mcrfpy.Entity] = None +grid: Optional[mcrfpy.Grid] = None +fov_layer = None +texture: Optional[mcrfpy.Texture] = None + +# Game state +game_over: bool = False + +# Message log +messages: list[tuple[str, mcrfpy.Color]] = [] + +# ============================================================================= +# Room Class +# ============================================================================= + +class RectangularRoom: + """A rectangular room with its position and size.""" + + def __init__(self, x: int, y: int, width: int, height: int): + self.x1 = x + self.y1 = y + self.x2 = x + width + self.y2 = y + height + + @property + def center(self) -> tuple[int, int]: + center_x = (self.x1 + self.x2) // 2 + center_y = (self.y1 + self.y2) // 2 + return center_x, center_y + + @property + def inner(self) -> tuple[slice, slice]: + return slice(self.x1 + 1, self.x2), slice(self.y1 + 1, self.y2) + + def intersects(self, other: "RectangularRoom") -> bool: + return ( + self.x1 <= other.x2 and + self.x2 >= other.x1 and + self.y1 <= other.y2 and + self.y2 >= other.y1 + ) + +# ============================================================================= +# Exploration Tracking +# ============================================================================= + +explored: list[list[bool]] = [] + +def init_explored() -> None: + """Initialize the explored array to all False.""" + global explored + explored = [[False for _ in range(GRID_WIDTH)] for _ in range(GRID_HEIGHT)] + +def mark_explored(x: int, y: int) -> None: + """Mark a tile as explored.""" + if 0 <= x < GRID_WIDTH and 0 <= y < GRID_HEIGHT: + explored[y][x] = True + +def is_explored(x: int, y: int) -> bool: + """Check if a tile has been explored.""" + if 0 <= x < GRID_WIDTH and 0 <= y < GRID_HEIGHT: + return explored[y][x] + return False + +# ============================================================================= +# Message Log +# ============================================================================= + +def add_message(text: str, color: mcrfpy.Color = None) -> None: + """Add a message to the log. + + Args: + text: The message text + color: Optional color (defaults to white) + """ + if color is None: + color = mcrfpy.Color(255, 255, 255) + + messages.append((text, color)) + + # Keep only the most recent messages + while len(messages) > MAX_MESSAGES: + messages.pop(0) + + # Update the message display + update_message_display() + +def update_message_display() -> None: + """Update the message log UI.""" + if message_log_caption is None: + return + + # Combine messages into a single string + lines = [] + for text, color in messages: + lines.append(text) + + message_log_caption.text = "\n".join(lines) + +def clear_messages() -> None: + """Clear all messages.""" + global messages + messages = [] + update_message_display() + +# ============================================================================= +# Dungeon Generation +# ============================================================================= + +def fill_with_walls(target_grid: mcrfpy.Grid) -> None: + """Fill the entire grid with wall tiles.""" + for y in range(GRID_HEIGHT): + for x in range(GRID_WIDTH): + cell = target_grid.at(x, y) + cell.tilesprite = SPRITE_WALL + cell.walkable = False + cell.transparent = False + +def carve_room(target_grid: mcrfpy.Grid, room: RectangularRoom) -> None: + """Carve out a room by setting its inner tiles to floor.""" + inner_x, inner_y = room.inner + for y in range(inner_y.start, inner_y.stop): + for x in range(inner_x.start, inner_x.stop): + if 0 <= x < GRID_WIDTH and 0 <= y < GRID_HEIGHT: + cell = target_grid.at(x, y) + cell.tilesprite = SPRITE_FLOOR + cell.walkable = True + cell.transparent = True + +def carve_tunnel_horizontal(target_grid: mcrfpy.Grid, x1: int, x2: int, y: int) -> None: + """Carve a horizontal tunnel.""" + for x in range(min(x1, x2), max(x1, x2) + 1): + if 0 <= x < GRID_WIDTH and 0 <= y < GRID_HEIGHT: + cell = target_grid.at(x, y) + cell.tilesprite = SPRITE_FLOOR + cell.walkable = True + cell.transparent = True + +def carve_tunnel_vertical(target_grid: mcrfpy.Grid, y1: int, y2: int, x: int) -> None: + """Carve a vertical tunnel.""" + for y in range(min(y1, y2), max(y1, y2) + 1): + if 0 <= x < GRID_WIDTH and 0 <= y < GRID_HEIGHT: + cell = target_grid.at(x, y) + cell.tilesprite = SPRITE_FLOOR + cell.walkable = True + cell.transparent = True + +def carve_l_tunnel( + target_grid: mcrfpy.Grid, + start: tuple[int, int], + end: tuple[int, int] +) -> None: + """Carve an L-shaped tunnel between two points.""" + x1, y1 = start + x2, y2 = end + + if random.random() < 0.5: + carve_tunnel_horizontal(target_grid, x1, x2, y1) + carve_tunnel_vertical(target_grid, y1, y2, x2) + else: + carve_tunnel_vertical(target_grid, y1, y2, x1) + carve_tunnel_horizontal(target_grid, x1, x2, y2) + +# ============================================================================= +# Entity Management +# ============================================================================= + +def spawn_enemy(target_grid: mcrfpy.Grid, x: int, y: int, enemy_type: str, tex: mcrfpy.Texture) -> mcrfpy.Entity: + """Spawn an enemy at the given position.""" + template = ENEMY_TEMPLATES[enemy_type] + + enemy = mcrfpy.Entity( + grid_pos=(x, y), + texture=tex, + sprite_index=template["sprite"] + ) + enemy.visible = False + + target_grid.entities.append(enemy) + + # Create Fighter component for this enemy + entity_data[enemy] = Fighter( + hp=template["hp"], + max_hp=template["hp"], + attack=template["attack"], + defense=template["defense"], + name=enemy_type.capitalize(), + is_player=False + ) + + return enemy + +def spawn_enemies_in_room(target_grid: mcrfpy.Grid, room: RectangularRoom, tex: mcrfpy.Texture) -> None: + """Spawn random enemies in a room.""" + num_enemies = random.randint(0, MAX_ENEMIES_PER_ROOM) + + for _ in range(num_enemies): + inner_x, inner_y = room.inner + x = random.randint(inner_x.start, inner_x.stop - 1) + y = random.randint(inner_y.start, inner_y.stop - 1) + + if get_entity_at(target_grid, x, y) is not None: + continue + + roll = random.random() + if roll < 0.6: + enemy_type = "goblin" + elif roll < 0.9: + enemy_type = "orc" + else: + enemy_type = "troll" + + spawn_enemy(target_grid, x, y, enemy_type, tex) + +def get_entity_at(target_grid: mcrfpy.Grid, x: int, y: int) -> Optional[mcrfpy.Entity]: + """Get any entity at the given position.""" + for entity in target_grid.entities: + if entity.grid_x == x and entity.grid_y == y: + # Check if this entity is alive (or is a non-Fighter entity) + if entity in entity_data: + if entity_data[entity].is_alive: + return entity + else: + return entity + return None + +def get_blocking_entity_at(target_grid: mcrfpy.Grid, x: int, y: int, exclude: mcrfpy.Entity = None) -> Optional[mcrfpy.Entity]: + """Get any living entity that blocks movement at the given position.""" + for entity in target_grid.entities: + if entity == exclude: + continue + if entity.grid_x == x and entity.grid_y == y: + if entity in entity_data and entity_data[entity].is_alive: + return entity + return None + +def remove_entity(target_grid: mcrfpy.Grid, entity: mcrfpy.Entity) -> None: + """Remove an entity from the grid and data storage.""" + # Remove from grid + target_grid.entities.remove(entity) + + # Remove from entity data + if entity in entity_data: + del entity_data[entity] + +def clear_enemies(target_grid: mcrfpy.Grid) -> None: + """Remove all enemies from the grid.""" + enemies_to_remove = [] + + for entity in target_grid.entities: + if entity in entity_data and not entity_data[entity].is_player: + enemies_to_remove.append(entity) + + for enemy in enemies_to_remove: + remove_entity(target_grid, enemy) + +# ============================================================================= +# Combat System +# ============================================================================= + +def calculate_damage(attacker: Fighter, defender: Fighter) -> int: + """Calculate damage dealt from attacker to defender. + + Args: + attacker: The attacking Fighter + defender: The defending Fighter + + Returns: + The amount of damage to deal (minimum 0) + """ + damage = max(0, attacker.attack - defender.defense) + return damage + +def perform_attack(attacker_entity: mcrfpy.Entity, defender_entity: mcrfpy.Entity) -> None: + """Execute an attack from one entity to another. + + Args: + attacker_entity: The entity performing the attack + defender_entity: The entity being attacked + """ + global game_over + + attacker = entity_data.get(attacker_entity) + defender = entity_data.get(defender_entity) + + if attacker is None or defender is None: + return + + # Calculate and apply damage + damage = calculate_damage(attacker, defender) + defender.take_damage(damage) + + # Generate combat message + if damage > 0: + if attacker.is_player: + add_message( + f"You hit the {defender.name} for {damage} damage!", + mcrfpy.Color(200, 200, 200) + ) + else: + add_message( + f"The {attacker.name} hits you for {damage} damage!", + mcrfpy.Color(255, 150, 150) + ) + else: + if attacker.is_player: + add_message( + f"You hit the {defender.name} but deal no damage.", + mcrfpy.Color(150, 150, 150) + ) + else: + add_message( + f"The {attacker.name} hits you but deals no damage.", + mcrfpy.Color(150, 150, 200) + ) + + # Check for death + if not defender.is_alive: + handle_death(defender_entity, defender) + +def handle_death(entity: mcrfpy.Entity, fighter: Fighter) -> None: + """Handle the death of an entity. + + Args: + entity: The entity that died + fighter: The Fighter component of the dead entity + """ + global game_over, grid + + if fighter.is_player: + # Player death + add_message("You have died!", mcrfpy.Color(255, 50, 50)) + add_message("Press R to restart or Escape to quit.", mcrfpy.Color(200, 200, 200)) + game_over = True + + # Change player sprite to corpse + entity.sprite_index = SPRITE_CORPSE + else: + # Enemy death + add_message(f"The {fighter.name} dies!", mcrfpy.Color(100, 255, 100)) + + # Replace with corpse + entity.sprite_index = SPRITE_CORPSE + + # Mark as dead (hp is already 0) + # Remove blocking but keep visual corpse + # Actually remove the entity and its data + remove_entity(grid, entity) + + # Update HP display + update_hp_display() + +# ============================================================================= +# Field of View +# ============================================================================= + +def update_entity_visibility(target_grid: mcrfpy.Grid) -> None: + """Update visibility of all entities based on FOV.""" + global player + + for entity in target_grid.entities: + if entity == player: + entity.visible = True + continue + + ex, ey = entity.grid_x, entity.grid_y + entity.visible = target_grid.is_in_fov(ex, ey) + +def update_fov(target_grid: mcrfpy.Grid, target_fov_layer, player_x: int, player_y: int) -> None: + """Update the field of view visualization.""" + target_grid.compute_fov((player_x, player_y), radius=FOV_RADIUS, algorithm=mcrfpy.FOV.SHADOW) + + for y in range(GRID_HEIGHT): + for x in range(GRID_WIDTH): + if target_grid.is_in_fov(x, y): + mark_explored(x, y) + target_fov_layer.set((x, y), COLOR_VISIBLE) + elif is_explored(x, y): + target_fov_layer.set((x, y), COLOR_DISCOVERED) + else: + target_fov_layer.set((x, y), COLOR_UNKNOWN) + + update_entity_visibility(target_grid) + +# ============================================================================= +# Movement and Actions +# ============================================================================= + +def can_move_to(target_grid: mcrfpy.Grid, x: int, y: int, mover: mcrfpy.Entity = None) -> bool: + """Check if a position is valid for movement.""" + if x < 0 or x >= GRID_WIDTH or y < 0 or y >= GRID_HEIGHT: + return False + + if not target_grid.at(x, y).walkable: + return False + + blocker = get_blocking_entity_at(target_grid, x, y, exclude=mover) + if blocker is not None: + return False + + return True + +def try_move_or_attack(dx: int, dy: int) -> None: + """Attempt to move the player or attack if blocked by enemy. + + Args: + dx: Change in X position (-1, 0, or 1) + dy: Change in Y position (-1, 0, or 1) + """ + global player, grid, fov_layer, game_over + + if game_over: + return + + px, py = player.grid_x, player.grid_y + target_x = px + dx + target_y = py + dy + + # Check bounds + if target_x < 0 or target_x >= GRID_WIDTH or target_y < 0 or target_y >= GRID_HEIGHT: + return + + # Check for blocking entity + blocker = get_blocking_entity_at(grid, target_x, target_y, exclude=player) + + if blocker is not None: + # Attack the blocking entity + perform_attack(player, blocker) + # After player attacks, enemies take their turn + enemy_turn() + elif grid.at(target_x, target_y).walkable: + # Move to the empty tile + player.grid_x = target_x + player.grid_y = target_y + pos_display.text = f"Position: ({target_x}, {target_y})" + + # Update FOV after movement + update_fov(grid, fov_layer, target_x, target_y) + + # Enemies take their turn after player moves + enemy_turn() + + # Update HP display + update_hp_display() + +# ============================================================================= +# Enemy AI +# ============================================================================= + +def enemy_turn() -> None: + """Execute enemy actions.""" + global player, grid, game_over + + if game_over: + return + + player_x, player_y = player.grid_x, player.grid_y + + # Collect enemies that can act + enemies = [] + for entity in grid.entities: + if entity == player: + continue + if entity in entity_data and entity_data[entity].is_alive: + enemies.append(entity) + + for enemy in enemies: + fighter = entity_data.get(enemy) + if fighter is None or not fighter.is_alive: + continue + + ex, ey = enemy.grid_x, enemy.grid_y + + # Only act if in player's FOV (aware of player) + if not grid.is_in_fov(ex, ey): + continue + + # Check if adjacent to player + dx = player_x - ex + dy = player_y - ey + + if abs(dx) <= 1 and abs(dy) <= 1 and (dx != 0 or dy != 0): + # Adjacent - attack! + perform_attack(enemy, player) + else: + # Not adjacent - try to move toward player + move_toward_player(enemy, ex, ey, player_x, player_y) + +def move_toward_player(enemy: mcrfpy.Entity, ex: int, ey: int, px: int, py: int) -> None: + """Move an enemy one step toward the player. + + Uses simple greedy movement - not true pathfinding. + """ + global grid + + # Calculate direction to player + dx = 0 + dy = 0 + + if px < ex: + dx = -1 + elif px > ex: + dx = 1 + + if py < ey: + dy = -1 + elif py > ey: + dy = 1 + + # Try to move in the desired direction + # First try the combined direction + new_x = ex + dx + new_y = ey + dy + + if can_move_to(grid, new_x, new_y, enemy): + enemy.grid_x = new_x + enemy.grid_y = new_y + elif dx != 0 and can_move_to(grid, ex + dx, ey, enemy): + # Try horizontal only + enemy.grid_x = ex + dx + elif dy != 0 and can_move_to(grid, ex, ey + dy, enemy): + # Try vertical only + enemy.grid_y = ey + dy + # If all fail, enemy stays in place + +# ============================================================================= +# UI Updates +# ============================================================================= + +def update_hp_display() -> None: + """Update the HP display in the UI.""" + global player + + if hp_display is None or player is None: + return + + if player in entity_data: + fighter = entity_data[player] + hp_display.text = f"HP: {fighter.hp}/{fighter.max_hp}" + + # Color based on health percentage + hp_percent = fighter.hp / fighter.max_hp + if hp_percent > 0.6: + hp_display.fill_color = mcrfpy.Color(100, 255, 100) + elif hp_percent > 0.3: + hp_display.fill_color = mcrfpy.Color(255, 255, 100) + else: + hp_display.fill_color = mcrfpy.Color(255, 100, 100) + +# ============================================================================= +# Game Setup +# ============================================================================= + +# Create the scene +scene = mcrfpy.Scene("game") + +# Load texture +texture = mcrfpy.Texture("assets/kenney_tinydungeon.png", 16, 16) + +# Create the grid +grid = mcrfpy.Grid( + pos=(50, 80), + size=(800, 480), + grid_size=(GRID_WIDTH, GRID_HEIGHT), + texture=texture, + zoom=1.0 +) + +# Generate initial dungeon structure +fill_with_walls(grid) +init_explored() + +rooms: list[RectangularRoom] = [] + +for _ in range(MAX_ROOMS): + room_width = random.randint(ROOM_MIN_SIZE, ROOM_MAX_SIZE) + room_height = random.randint(ROOM_MIN_SIZE, ROOM_MAX_SIZE) + x = random.randint(1, GRID_WIDTH - room_width - 2) + y = random.randint(1, GRID_HEIGHT - room_height - 2) + + new_room = RectangularRoom(x, y, room_width, room_height) + + overlaps = False + for other_room in rooms: + if new_room.intersects(other_room): + overlaps = True + break + + if overlaps: + continue + + carve_room(grid, new_room) + + if rooms: + carve_l_tunnel(grid, new_room.center, rooms[-1].center) + + rooms.append(new_room) + +# Get player starting position +if rooms: + player_start_x, player_start_y = rooms[0].center +else: + player_start_x, player_start_y = GRID_WIDTH // 2, GRID_HEIGHT // 2 + +# Add FOV layer +fov_layer = mcrfpy.ColorLayer(name="fov", z_index=-1) +grid.add_layer(fov_layer) +for y in range(GRID_HEIGHT): + for x in range(GRID_WIDTH): + fov_layer.set((x, y), COLOR_UNKNOWN) + +# Create the player +player = mcrfpy.Entity( + grid_pos=(player_start_x, player_start_y), + texture=texture, + sprite_index=SPRITE_PLAYER +) +grid.entities.append(player) + +# Create player Fighter component +entity_data[player] = Fighter( + hp=30, + max_hp=30, + attack=5, + defense=2, + name="Player", + is_player=True +) + +# Spawn enemies in all rooms except the first +for i, room in enumerate(rooms): + if i == 0: + continue + spawn_enemies_in_room(grid, room, texture) + +# Calculate initial FOV +update_fov(grid, fov_layer, player_start_x, player_start_y) + +# Add grid to scene +scene.children.append(grid) + +# ============================================================================= +# UI Elements +# ============================================================================= + +title = mcrfpy.Caption( + pos=(50, 15), + text="Part 6: Combat System" +) +title.fill_color = mcrfpy.Color(255, 255, 255) +title.font_size = 24 +scene.children.append(title) + +instructions = mcrfpy.Caption( + pos=(50, 50), + text="WASD/Arrows: Move/Attack | R: Restart | Escape: Quit" +) +instructions.fill_color = mcrfpy.Color(180, 180, 180) +instructions.font_size = 16 +scene.children.append(instructions) + +# Position display +pos_display = mcrfpy.Caption( + pos=(50, 580), + text=f"Position: ({player.grid_x}, {player.grid_y})" +) +pos_display.fill_color = mcrfpy.Color(200, 200, 100) +pos_display.font_size = 16 +scene.children.append(pos_display) + +# HP display +hp_display = mcrfpy.Caption( + pos=(300, 580), + text="HP: 30/30" +) +hp_display.fill_color = mcrfpy.Color(100, 255, 100) +hp_display.font_size = 16 +scene.children.append(hp_display) + +# Message log (positioned below the grid) +message_log_caption = mcrfpy.Caption( + pos=(50, 610), + text="" +) +message_log_caption.fill_color = mcrfpy.Color(200, 200, 200) +message_log_caption.font_size = 14 +scene.children.append(message_log_caption) + +# Initial message +add_message("Welcome to the dungeon! Find and defeat the enemies.", mcrfpy.Color(100, 100, 255)) + +# ============================================================================= +# Input Handling +# ============================================================================= + +def restart_game() -> None: + """Restart the game with a new dungeon.""" + global player, grid, fov_layer, game_over, entity_data, rooms + + game_over = False + + # Clear all entities and data + entity_data.clear() + + # Remove all entities from grid + while len(grid.entities) > 0: + grid.entities.pop() + + # Regenerate dungeon + fill_with_walls(grid) + init_explored() + clear_messages() + + rooms = [] + + for _ in range(MAX_ROOMS): + room_width = random.randint(ROOM_MIN_SIZE, ROOM_MAX_SIZE) + room_height = random.randint(ROOM_MIN_SIZE, ROOM_MAX_SIZE) + x = random.randint(1, GRID_WIDTH - room_width - 2) + y = random.randint(1, GRID_HEIGHT - room_height - 2) + + new_room = RectangularRoom(x, y, room_width, room_height) + + overlaps = False + for other_room in rooms: + if new_room.intersects(other_room): + overlaps = True + break + + if overlaps: + continue + + carve_room(grid, new_room) + + if rooms: + carve_l_tunnel(grid, new_room.center, rooms[-1].center) + + rooms.append(new_room) + + # Get new player starting position + if rooms: + new_x, new_y = rooms[0].center + else: + new_x, new_y = GRID_WIDTH // 2, GRID_HEIGHT // 2 + + # Recreate player + player = mcrfpy.Entity( + grid_pos=(new_x, new_y), + texture=texture, + sprite_index=SPRITE_PLAYER + ) + grid.entities.append(player) + + entity_data[player] = Fighter( + hp=30, + max_hp=30, + attack=5, + defense=2, + name="Player", + is_player=True + ) + + # Spawn enemies + for i, room in enumerate(rooms): + if i == 0: + continue + spawn_enemies_in_room(grid, room, texture) + + # Reset FOV layer + for y in range(GRID_HEIGHT): + for x in range(GRID_WIDTH): + fov_layer.set((x, y), COLOR_UNKNOWN) + + # Update displays + update_fov(grid, fov_layer, new_x, new_y) + pos_display.text = f"Position: ({new_x}, {new_y})" + update_hp_display() + + add_message("A new adventure begins!", mcrfpy.Color(100, 100, 255)) + +def handle_keys(key: mcrfpy.Key, action: mcrfpy.InputState) -> None: + """Handle keyboard input.""" + global game_over + + if action != mcrfpy.InputState.PRESSED: + return + + # Handle restart + if key == mcrfpy.Key.R: + restart_game() + return + + if key == mcrfpy.Key.ESCAPE: + mcrfpy.exit() + return + + # Ignore other input if game is over + if game_over: + return + + # Movement and attack + if key == mcrfpy.Key.W or key == mcrfpy.Key.UP: + try_move_or_attack(0, -1) + elif key == mcrfpy.Key.S or key == mcrfpy.Key.DOWN: + try_move_or_attack(0, 1) + elif key == mcrfpy.Key.A or key == mcrfpy.Key.LEFT: + try_move_or_attack(-1, 0) + elif key == mcrfpy.Key.D or key == mcrfpy.Key.RIGHT: + try_move_or_attack(1, 0) + +scene.on_key = handle_keys + +# ============================================================================= +# Start the Game +# ============================================================================= + +scene.activate() +print("Part 6 loaded! Combat is now active. Good luck!") diff --git a/tests/snippets/277_part_07_ui.py b/tests/snippets/277_part_07_ui.py new file mode 100644 index 0000000..75a0cde --- /dev/null +++ b/tests/snippets/277_part_07_ui.py @@ -0,0 +1,1028 @@ +# mcrf: objects=[Caption,Color,ColorLayer,Entity,FOV,Frame,Grid,InputState,Key,Scene,Texture] verified=0.2.8-dev status=ok +"""McRogueFace Tutorial - Part 7: User Interface + +Create a polished UI with health bar, message log, and stats display. +""" +import mcrfpy +import random +from dataclasses import dataclass +from typing import Optional + +# ============================================================================= +# Constants +# ============================================================================= + +# Sprite indices for CP437 tileset +SPRITE_WALL = 35 # '#' - wall +SPRITE_FLOOR = 46 # '.' - floor +SPRITE_PLAYER = 64 # '@' - player +SPRITE_CORPSE = 37 # '%' - remains + +# Enemy sprites +SPRITE_GOBLIN = 103 # 'g' +SPRITE_ORC = 111 # 'o' +SPRITE_TROLL = 116 # 't' + +# Grid dimensions +GRID_WIDTH = 50 +GRID_HEIGHT = 30 + +# Room generation parameters +ROOM_MIN_SIZE = 6 +ROOM_MAX_SIZE = 12 +MAX_ROOMS = 8 + +# Enemy spawn parameters +MAX_ENEMIES_PER_ROOM = 3 + +# FOV settings +FOV_RADIUS = 8 + +# Visibility colors +COLOR_VISIBLE = mcrfpy.Color(0, 0, 0, 0) +COLOR_DISCOVERED = mcrfpy.Color(0, 0, 40, 180) +COLOR_UNKNOWN = mcrfpy.Color(0, 0, 0, 255) + +# Message colors +COLOR_PLAYER_ATTACK = mcrfpy.Color(200, 200, 200) +COLOR_ENEMY_ATTACK = mcrfpy.Color(255, 150, 150) +COLOR_PLAYER_DEATH = mcrfpy.Color(255, 50, 50) +COLOR_ENEMY_DEATH = mcrfpy.Color(100, 255, 100) +COLOR_HEAL = mcrfpy.Color(100, 255, 100) +COLOR_INFO = mcrfpy.Color(100, 100, 255) +COLOR_WARNING = mcrfpy.Color(255, 200, 50) + +# UI Layout constants +UI_TOP_HEIGHT = 60 +UI_BOTTOM_HEIGHT = 150 +GAME_AREA_Y = UI_TOP_HEIGHT +GAME_AREA_HEIGHT = 768 - UI_TOP_HEIGHT - UI_BOTTOM_HEIGHT + +# ============================================================================= +# Fighter Component +# ============================================================================= + +@dataclass +class Fighter: + """Combat stats for an entity.""" + hp: int + max_hp: int + attack: int + defense: int + name: str + is_player: bool = False + + @property + def is_alive(self) -> bool: + return self.hp > 0 + + def take_damage(self, amount: int) -> int: + actual_damage = min(self.hp, amount) + self.hp -= actual_damage + return actual_damage + + def heal(self, amount: int) -> int: + actual_heal = min(self.max_hp - self.hp, amount) + self.hp += actual_heal + return actual_heal + +# ============================================================================= +# Enemy Templates +# ============================================================================= + +ENEMY_TEMPLATES = { + "goblin": { + "sprite": SPRITE_GOBLIN, + "hp": 6, + "attack": 3, + "defense": 0 + }, + "orc": { + "sprite": SPRITE_ORC, + "hp": 10, + "attack": 4, + "defense": 1 + }, + "troll": { + "sprite": SPRITE_TROLL, + "hp": 16, + "attack": 6, + "defense": 2 + } +} + +# ============================================================================= +# Message Log System +# ============================================================================= + +class MessageLog: + """A message log that displays recent game messages with colors.""" + + def __init__(self, x: int, y: int, width: int, height: int, max_messages: int = 6): + """Create a new message log. + + Args: + x: X position of the log + y: Y position of the log + width: Width of the log area + height: Height of the log area + max_messages: Maximum number of messages to display + """ + self.x = x + self.y = y + self.width = width + self.height = height + self.max_messages = max_messages + self.messages: list[tuple[str, mcrfpy.Color]] = [] + self.captions: list[mcrfpy.Caption] = [] + + # Create the background frame + self.frame = mcrfpy.Frame( + pos=(x, y), + size=(width, height) + ) + self.frame.fill_color = mcrfpy.Color(20, 20, 30, 200) + self.frame.outline = 2 + self.frame.outline_color = mcrfpy.Color(80, 80, 100) + + # Create caption for each message line + line_height = 20 + for i in range(max_messages): + caption = mcrfpy.Caption( + pos=(x + 10, y + 5 + i * line_height), + text="" + ) + caption.font_size = 14 + caption.fill_color = mcrfpy.Color(200, 200, 200) + self.captions.append(caption) + + def add_to_scene(self, scene: mcrfpy.Scene) -> None: + """Add the message log UI elements to a scene.""" + scene.children.append(self.frame) + for caption in self.captions: + scene.children.append(caption) + + def add(self, text: str, color: mcrfpy.Color = None) -> None: + """Add a message to the log. + + Args: + text: The message text + color: Optional color (defaults to white) + """ + if color is None: + color = mcrfpy.Color(200, 200, 200) + + self.messages.append((text, color)) + + # Keep only the most recent messages + while len(self.messages) > self.max_messages: + self.messages.pop(0) + + self._refresh() + + def _refresh(self) -> None: + """Update the caption displays with current messages.""" + for i, caption in enumerate(self.captions): + if i < len(self.messages): + text, color = self.messages[i] + caption.text = text + caption.fill_color = color + else: + caption.text = "" + + def clear(self) -> None: + """Clear all messages.""" + self.messages.clear() + self._refresh() + +# ============================================================================= +# Health Bar System +# ============================================================================= + +class HealthBar: + """A visual health bar using nested frames.""" + + def __init__(self, x: int, y: int, width: int, height: int): + """Create a new health bar. + + Args: + x: X position + y: Y position + width: Total width of the health bar + height: Height of the health bar + """ + self.x = x + self.y = y + self.width = width + self.height = height + self.max_hp = 30 + self.current_hp = 30 + + # Background frame (dark red - shows when damaged) + self.bg_frame = mcrfpy.Frame( + pos=(x, y), + size=(width, height) + ) + self.bg_frame.fill_color = mcrfpy.Color(80, 0, 0) + self.bg_frame.outline = 2 + self.bg_frame.outline_color = mcrfpy.Color(150, 150, 150) + + # Foreground frame (the actual health - shrinks as HP decreases) + self.fg_frame = mcrfpy.Frame( + pos=(x + 2, y + 2), + size=(width - 4, height - 4) + ) + self.fg_frame.fill_color = mcrfpy.Color(0, 180, 0) + self.fg_frame.outline = 0 + + # HP text label + self.label = mcrfpy.Caption( + pos=(x + 5, y + 2), + text=f"HP: {self.current_hp}/{self.max_hp}" + ) + self.label.font_size = 16 + self.label.fill_color = mcrfpy.Color(255, 255, 255) + + def add_to_scene(self, scene: mcrfpy.Scene) -> None: + """Add the health bar UI elements to a scene.""" + scene.children.append(self.bg_frame) + scene.children.append(self.fg_frame) + scene.children.append(self.label) + + def update(self, current_hp: int, max_hp: int) -> None: + """Update the health bar display. + + Args: + current_hp: Current HP value + max_hp: Maximum HP value + """ + self.current_hp = current_hp + self.max_hp = max_hp + + # Calculate fill percentage + percent = max(0, current_hp / max_hp) if max_hp > 0 else 0 + + # Update the foreground width + inner_width = self.width - 4 + self.fg_frame.resize(int(inner_width * percent), self.height - 4) + + # Update the label + self.label.text = f"HP: {current_hp}/{max_hp}" + + # Update color based on health percentage + if percent > 0.6: + self.fg_frame.fill_color = mcrfpy.Color(0, 180, 0) # Green + elif percent > 0.3: + self.fg_frame.fill_color = mcrfpy.Color(180, 180, 0) # Yellow + else: + self.fg_frame.fill_color = mcrfpy.Color(180, 0, 0) # Red + +# ============================================================================= +# Stats Panel +# ============================================================================= + +class StatsPanel: + """A panel displaying player stats and dungeon info.""" + + def __init__(self, x: int, y: int, width: int, height: int): + """Create a new stats panel. + + Args: + x: X position + y: Y position + width: Panel width + height: Panel height + """ + self.x = x + self.y = y + self.width = width + self.height = height + + # Background frame + self.frame = mcrfpy.Frame( + pos=(x, y), + size=(width, height) + ) + self.frame.fill_color = mcrfpy.Color(20, 20, 30, 200) + self.frame.outline = 2 + self.frame.outline_color = mcrfpy.Color(80, 80, 100) + + # Dungeon level caption + self.level_caption = mcrfpy.Caption( + pos=(x + 10, y + 10), + text="Dungeon Level: 1" + ) + self.level_caption.font_size = 16 + self.level_caption.fill_color = mcrfpy.Color(200, 200, 255) + + # Attack stat caption + self.attack_caption = mcrfpy.Caption( + pos=(x + 10, y + 35), + text="Attack: 5" + ) + self.attack_caption.font_size = 14 + self.attack_caption.fill_color = mcrfpy.Color(255, 200, 150) + + # Defense stat caption + self.defense_caption = mcrfpy.Caption( + pos=(x + 120, y + 35), + text="Defense: 2" + ) + self.defense_caption.font_size = 14 + self.defense_caption.fill_color = mcrfpy.Color(150, 200, 255) + + def add_to_scene(self, scene: mcrfpy.Scene) -> None: + """Add the stats panel UI elements to a scene.""" + scene.children.append(self.frame) + scene.children.append(self.level_caption) + scene.children.append(self.attack_caption) + scene.children.append(self.defense_caption) + + def update(self, dungeon_level: int, attack: int, defense: int) -> None: + """Update the stats panel display. + + Args: + dungeon_level: Current dungeon level + attack: Player attack stat + defense: Player defense stat + """ + self.level_caption.text = f"Dungeon Level: {dungeon_level}" + self.attack_caption.text = f"Attack: {attack}" + self.defense_caption.text = f"Defense: {defense}" + +# ============================================================================= +# Global State +# ============================================================================= + +entity_data: dict[mcrfpy.Entity, Fighter] = {} +player: Optional[mcrfpy.Entity] = None +grid: Optional[mcrfpy.Grid] = None +fov_layer = None +texture: Optional[mcrfpy.Texture] = None +game_over: bool = False +dungeon_level: int = 1 + +# UI components +message_log: Optional[MessageLog] = None +health_bar: Optional[HealthBar] = None +stats_panel: Optional[StatsPanel] = None + +# ============================================================================= +# Room Class +# ============================================================================= + +class RectangularRoom: + """A rectangular room with its position and size.""" + + def __init__(self, x: int, y: int, width: int, height: int): + self.x1 = x + self.y1 = y + self.x2 = x + width + self.y2 = y + height + + @property + def center(self) -> tuple[int, int]: + center_x = (self.x1 + self.x2) // 2 + center_y = (self.y1 + self.y2) // 2 + return center_x, center_y + + @property + def inner(self) -> tuple[slice, slice]: + return slice(self.x1 + 1, self.x2), slice(self.y1 + 1, self.y2) + + def intersects(self, other: "RectangularRoom") -> bool: + return ( + self.x1 <= other.x2 and + self.x2 >= other.x1 and + self.y1 <= other.y2 and + self.y2 >= other.y1 + ) + +# ============================================================================= +# Exploration Tracking +# ============================================================================= + +explored: list[list[bool]] = [] + +def init_explored() -> None: + global explored + explored = [[False for _ in range(GRID_WIDTH)] for _ in range(GRID_HEIGHT)] + +def mark_explored(x: int, y: int) -> None: + if 0 <= x < GRID_WIDTH and 0 <= y < GRID_HEIGHT: + explored[y][x] = True + +def is_explored(x: int, y: int) -> bool: + if 0 <= x < GRID_WIDTH and 0 <= y < GRID_HEIGHT: + return explored[y][x] + return False + +# ============================================================================= +# Dungeon Generation +# ============================================================================= + +def fill_with_walls(target_grid: mcrfpy.Grid) -> None: + for y in range(GRID_HEIGHT): + for x in range(GRID_WIDTH): + cell = target_grid.at(x, y) + cell.tilesprite = SPRITE_WALL + cell.walkable = False + cell.transparent = False + +def carve_room(target_grid: mcrfpy.Grid, room: RectangularRoom) -> None: + inner_x, inner_y = room.inner + for y in range(inner_y.start, inner_y.stop): + for x in range(inner_x.start, inner_x.stop): + if 0 <= x < GRID_WIDTH and 0 <= y < GRID_HEIGHT: + cell = target_grid.at(x, y) + cell.tilesprite = SPRITE_FLOOR + cell.walkable = True + cell.transparent = True + +def carve_tunnel_horizontal(target_grid: mcrfpy.Grid, x1: int, x2: int, y: int) -> None: + for x in range(min(x1, x2), max(x1, x2) + 1): + if 0 <= x < GRID_WIDTH and 0 <= y < GRID_HEIGHT: + cell = target_grid.at(x, y) + cell.tilesprite = SPRITE_FLOOR + cell.walkable = True + cell.transparent = True + +def carve_tunnel_vertical(target_grid: mcrfpy.Grid, y1: int, y2: int, x: int) -> None: + for y in range(min(y1, y2), max(y1, y2) + 1): + if 0 <= x < GRID_WIDTH and 0 <= y < GRID_HEIGHT: + cell = target_grid.at(x, y) + cell.tilesprite = SPRITE_FLOOR + cell.walkable = True + cell.transparent = True + +def carve_l_tunnel( + target_grid: mcrfpy.Grid, + start: tuple[int, int], + end: tuple[int, int] +) -> None: + x1, y1 = start + x2, y2 = end + + if random.random() < 0.5: + carve_tunnel_horizontal(target_grid, x1, x2, y1) + carve_tunnel_vertical(target_grid, y1, y2, x2) + else: + carve_tunnel_vertical(target_grid, y1, y2, x1) + carve_tunnel_horizontal(target_grid, x1, x2, y2) + +# ============================================================================= +# Entity Management +# ============================================================================= + +def spawn_enemy(target_grid: mcrfpy.Grid, x: int, y: int, enemy_type: str, tex: mcrfpy.Texture) -> mcrfpy.Entity: + template = ENEMY_TEMPLATES[enemy_type] + + enemy = mcrfpy.Entity( + grid_pos=(x, y), + texture=tex, + sprite_index=template["sprite"] + ) + enemy.visible = False + + target_grid.entities.append(enemy) + + entity_data[enemy] = Fighter( + hp=template["hp"], + max_hp=template["hp"], + attack=template["attack"], + defense=template["defense"], + name=enemy_type.capitalize(), + is_player=False + ) + + return enemy + +def spawn_enemies_in_room(target_grid: mcrfpy.Grid, room: RectangularRoom, tex: mcrfpy.Texture) -> None: + num_enemies = random.randint(0, MAX_ENEMIES_PER_ROOM) + + for _ in range(num_enemies): + inner_x, inner_y = room.inner + x = random.randint(inner_x.start, inner_x.stop - 1) + y = random.randint(inner_y.start, inner_y.stop - 1) + + if get_entity_at(target_grid, x, y) is not None: + continue + + roll = random.random() + if roll < 0.6: + enemy_type = "goblin" + elif roll < 0.9: + enemy_type = "orc" + else: + enemy_type = "troll" + + spawn_enemy(target_grid, x, y, enemy_type, tex) + +def get_entity_at(target_grid: mcrfpy.Grid, x: int, y: int) -> Optional[mcrfpy.Entity]: + for entity in target_grid.entities: + if int(entity.grid_x) == x and int(entity.grid_y) == y: + if entity in entity_data: + if entity_data[entity].is_alive: + return entity + else: + return entity + return None + +def get_blocking_entity_at(target_grid: mcrfpy.Grid, x: int, y: int, exclude: mcrfpy.Entity = None) -> Optional[mcrfpy.Entity]: + for entity in target_grid.entities: + if entity == exclude: + continue + if int(entity.grid_x) == x and int(entity.grid_y) == y: + if entity in entity_data and entity_data[entity].is_alive: + return entity + return None + +def remove_entity(target_grid: mcrfpy.Grid, entity: mcrfpy.Entity) -> None: + for i, e in enumerate(target_grid.entities): + if e == entity: + target_grid.entities.remove(entity) + break + if entity in entity_data: + del entity_data[entity] + +def clear_enemies(target_grid: mcrfpy.Grid) -> None: + enemies_to_remove = [] + for entity in target_grid.entities: + if entity in entity_data and not entity_data[entity].is_player: + enemies_to_remove.append(entity) + for enemy in enemies_to_remove: + remove_entity(target_grid, enemy) + +# ============================================================================= +# Combat System +# ============================================================================= + +def calculate_damage(attacker: Fighter, defender: Fighter) -> int: + return max(0, attacker.attack - defender.defense) + +def perform_attack(attacker_entity: mcrfpy.Entity, defender_entity: mcrfpy.Entity) -> None: + global game_over + + attacker = entity_data.get(attacker_entity) + defender = entity_data.get(defender_entity) + + if attacker is None or defender is None: + return + + damage = calculate_damage(attacker, defender) + defender.take_damage(damage) + + if damage > 0: + if attacker.is_player: + message_log.add( + f"You hit the {defender.name} for {damage} damage!", + COLOR_PLAYER_ATTACK + ) + else: + message_log.add( + f"The {attacker.name} hits you for {damage} damage!", + COLOR_ENEMY_ATTACK + ) + else: + if attacker.is_player: + message_log.add( + f"You hit the {defender.name} but deal no damage.", + mcrfpy.Color(150, 150, 150) + ) + else: + message_log.add( + f"The {attacker.name} hits but deals no damage.", + mcrfpy.Color(150, 150, 200) + ) + + if not defender.is_alive: + handle_death(defender_entity, defender) + + update_ui() + +def handle_death(entity: mcrfpy.Entity, fighter: Fighter) -> None: + global game_over, grid + + if fighter.is_player: + message_log.add("You have died!", COLOR_PLAYER_DEATH) + message_log.add("Press R to restart or Escape to quit.", COLOR_INFO) + game_over = True + entity.sprite_index = SPRITE_CORPSE + else: + message_log.add(f"The {fighter.name} dies!", COLOR_ENEMY_DEATH) + remove_entity(grid, entity) + +# ============================================================================= +# Field of View +# ============================================================================= + +def update_entity_visibility(target_grid: mcrfpy.Grid) -> None: + global player + + for entity in target_grid.entities: + if entity == player: + entity.visible = True + continue + + ex, ey = int(entity.grid_x), int(entity.grid_y) + entity.visible = target_grid.is_in_fov(ex, ey) + +def update_fov(target_grid: mcrfpy.Grid, target_fov_layer, player_x: int, player_y: int) -> None: + target_grid.compute_fov((player_x, player_y), radius=FOV_RADIUS, algorithm=mcrfpy.FOV.SHADOW) + + for y in range(GRID_HEIGHT): + for x in range(GRID_WIDTH): + if target_grid.is_in_fov(x, y): + mark_explored(x, y) + target_fov_layer.set((x, y), COLOR_VISIBLE) + elif is_explored(x, y): + target_fov_layer.set((x, y), COLOR_DISCOVERED) + else: + target_fov_layer.set((x, y), COLOR_UNKNOWN) + + update_entity_visibility(target_grid) + +# ============================================================================= +# Movement and Actions +# ============================================================================= + +def can_move_to(target_grid: mcrfpy.Grid, x: int, y: int, mover: mcrfpy.Entity = None) -> bool: + if x < 0 or x >= GRID_WIDTH or y < 0 or y >= GRID_HEIGHT: + return False + + if not target_grid.at(x, y).walkable: + return False + + blocker = get_blocking_entity_at(target_grid, x, y, exclude=mover) + if blocker is not None: + return False + + return True + +def try_move_or_attack(dx: int, dy: int) -> None: + global player, grid, fov_layer, game_over + + if game_over: + return + + px, py = int(player.grid_x), int(player.grid_y) + target_x = px + dx + target_y = py + dy + + if target_x < 0 or target_x >= GRID_WIDTH or target_y < 0 or target_y >= GRID_HEIGHT: + return + + blocker = get_blocking_entity_at(grid, target_x, target_y, exclude=player) + + if blocker is not None: + perform_attack(player, blocker) + enemy_turn() + elif grid.at(target_x, target_y).walkable: + player.grid_x = target_x + player.grid_y = target_y + update_fov(grid, fov_layer, target_x, target_y) + enemy_turn() + + update_ui() + +# ============================================================================= +# Enemy AI +# ============================================================================= + +def enemy_turn() -> None: + global player, grid, game_over + + if game_over: + return + + player_x, player_y = int(player.grid_x), int(player.grid_y) + + enemies = [] + for entity in grid.entities: + if entity == player: + continue + if entity in entity_data and entity_data[entity].is_alive: + enemies.append(entity) + + for enemy in enemies: + fighter = entity_data.get(enemy) + if fighter is None or not fighter.is_alive: + continue + + ex, ey = int(enemy.grid_x), int(enemy.grid_y) + + if not grid.is_in_fov(ex, ey): + continue + + dx = player_x - ex + dy = player_y - ey + + if abs(dx) <= 1 and abs(dy) <= 1 and (dx != 0 or dy != 0): + perform_attack(enemy, player) + else: + move_toward_player(enemy, ex, ey, player_x, player_y) + +def move_toward_player(enemy: mcrfpy.Entity, ex: int, ey: int, px: int, py: int) -> None: + global grid + + dx = 0 + dy = 0 + + if px < ex: + dx = -1 + elif px > ex: + dx = 1 + + if py < ey: + dy = -1 + elif py > ey: + dy = 1 + + new_x = ex + dx + new_y = ey + dy + + if can_move_to(grid, new_x, new_y, enemy): + enemy.grid_x = new_x + enemy.grid_y = new_y + elif dx != 0 and can_move_to(grid, ex + dx, ey, enemy): + enemy.grid_x = ex + dx + elif dy != 0 and can_move_to(grid, ex, ey + dy, enemy): + enemy.grid_y = ey + dy + +# ============================================================================= +# UI Updates +# ============================================================================= + +def update_ui() -> None: + """Update all UI components.""" + global player, health_bar, stats_panel, dungeon_level + + if player in entity_data: + fighter = entity_data[player] + health_bar.update(fighter.hp, fighter.max_hp) + stats_panel.update(dungeon_level, fighter.attack, fighter.defense) + +# ============================================================================= +# Game Setup +# ============================================================================= + +# Create the scene +scene = mcrfpy.Scene("game") + +# Load texture +texture = mcrfpy.Texture("assets/kenney_tinydungeon.png", 16, 16) + +# Create the grid (positioned to leave room for UI) +grid = mcrfpy.Grid( + pos=(20, GAME_AREA_Y), + size=(800, GAME_AREA_HEIGHT - 20), + grid_size=(GRID_WIDTH, GRID_HEIGHT), + texture=texture, + zoom=1.0 +) + +# Generate initial dungeon structure +fill_with_walls(grid) +init_explored() + +rooms: list[RectangularRoom] = [] + +for _ in range(MAX_ROOMS): + room_width = random.randint(ROOM_MIN_SIZE, ROOM_MAX_SIZE) + room_height = random.randint(ROOM_MIN_SIZE, ROOM_MAX_SIZE) + x = random.randint(1, GRID_WIDTH - room_width - 2) + y = random.randint(1, GRID_HEIGHT - room_height - 2) + + new_room = RectangularRoom(x, y, room_width, room_height) + + overlaps = False + for other_room in rooms: + if new_room.intersects(other_room): + overlaps = True + break + + if overlaps: + continue + + carve_room(grid, new_room) + + if rooms: + carve_l_tunnel(grid, new_room.center, rooms[-1].center) + + rooms.append(new_room) + +# Get player starting position +if rooms: + player_start_x, player_start_y = rooms[0].center +else: + player_start_x, player_start_y = GRID_WIDTH // 2, GRID_HEIGHT // 2 + +# Add FOV layer +fov_layer = mcrfpy.ColorLayer(name="fov", z_index=-1) +grid.add_layer(fov_layer) +for y in range(GRID_HEIGHT): + for x in range(GRID_WIDTH): + fov_layer.set((x, y), COLOR_UNKNOWN) + +# Create the player +player = mcrfpy.Entity( + grid_pos=(player_start_x, player_start_y), + texture=texture, + sprite_index=SPRITE_PLAYER +) +grid.entities.append(player) + +entity_data[player] = Fighter( + hp=30, + max_hp=30, + attack=5, + defense=2, + name="Player", + is_player=True +) + +# Spawn enemies +for i, room in enumerate(rooms): + if i == 0: + continue + spawn_enemies_in_room(grid, room, texture) + +# Calculate initial FOV +update_fov(grid, fov_layer, player_start_x, player_start_y) + +# Add grid to scene +scene.children.append(grid) + +# ============================================================================= +# Create UI Elements +# ============================================================================= + +# Title bar +title = mcrfpy.Caption( + pos=(20, 10), + text="Part 7: User Interface" +) +title.fill_color = mcrfpy.Color(255, 255, 255) +title.font_size = 24 +scene.children.append(title) + +# Instructions +instructions = mcrfpy.Caption( + pos=(300, 15), + text="WASD/Arrows: Move/Attack | R: Restart | Escape: Quit" +) +instructions.fill_color = mcrfpy.Color(180, 180, 180) +instructions.font_size = 14 +scene.children.append(instructions) + +# Health Bar (top right area) +health_bar = HealthBar( + x=700, + y=10, + width=300, + height=30 +) +health_bar.add_to_scene(scene) + +# Stats Panel (below health bar) +stats_panel = StatsPanel( + x=830, + y=GAME_AREA_Y, + width=180, + height=80 +) +stats_panel.add_to_scene(scene) + +# Message Log (bottom of screen) +message_log = MessageLog( + x=20, + y=768 - UI_BOTTOM_HEIGHT + 10, + width=800, + height=UI_BOTTOM_HEIGHT - 20, + max_messages=6 +) +message_log.add_to_scene(scene) + +# Initial messages +message_log.add("Welcome to the dungeon!", COLOR_INFO) +message_log.add("Find and defeat all enemies to progress.", COLOR_INFO) + +# Initialize UI +update_ui() + +# ============================================================================= +# Input Handling +# ============================================================================= + +def restart_game() -> None: + """Restart the game with a new dungeon.""" + global player, grid, fov_layer, game_over, entity_data, rooms, dungeon_level + + game_over = False + dungeon_level = 1 + + entity_data.clear() + + while len(grid.entities) > 0: + grid.entities.pop() + + fill_with_walls(grid) + init_explored() + message_log.clear() + + rooms = [] + + for _ in range(MAX_ROOMS): + room_width = random.randint(ROOM_MIN_SIZE, ROOM_MAX_SIZE) + room_height = random.randint(ROOM_MIN_SIZE, ROOM_MAX_SIZE) + x = random.randint(1, GRID_WIDTH - room_width - 2) + y = random.randint(1, GRID_HEIGHT - room_height - 2) + + new_room = RectangularRoom(x, y, room_width, room_height) + + overlaps = False + for other_room in rooms: + if new_room.intersects(other_room): + overlaps = True + break + + if overlaps: + continue + + carve_room(grid, new_room) + + if rooms: + carve_l_tunnel(grid, new_room.center, rooms[-1].center) + + rooms.append(new_room) + + if rooms: + new_x, new_y = rooms[0].center + else: + new_x, new_y = GRID_WIDTH // 2, GRID_HEIGHT // 2 + + player = mcrfpy.Entity( + grid_pos=(new_x, new_y), + texture=texture, + sprite_index=SPRITE_PLAYER + ) + grid.entities.append(player) + + entity_data[player] = Fighter( + hp=30, + max_hp=30, + attack=5, + defense=2, + name="Player", + is_player=True + ) + + for i, room in enumerate(rooms): + if i == 0: + continue + spawn_enemies_in_room(grid, room, texture) + + for y in range(GRID_HEIGHT): + for x in range(GRID_WIDTH): + fov_layer.set((x, y), COLOR_UNKNOWN) + + update_fov(grid, fov_layer, new_x, new_y) + + message_log.add("A new adventure begins!", COLOR_INFO) + + update_ui() + +def handle_keys(key: mcrfpy.Key, action: mcrfpy.InputState) -> None: + global game_over + + if action != mcrfpy.InputState.PRESSED: + return + + if key == mcrfpy.Key.R: + restart_game() + return + + if key == mcrfpy.Key.ESCAPE: + mcrfpy.exit() + return + + if game_over: + return + + if key == mcrfpy.Key.W or key == mcrfpy.Key.UP: + try_move_or_attack(0, -1) + elif key == mcrfpy.Key.S or key == mcrfpy.Key.DOWN: + try_move_or_attack(0, 1) + elif key == mcrfpy.Key.A or key == mcrfpy.Key.LEFT: + try_move_or_attack(-1, 0) + elif key == mcrfpy.Key.D or key == mcrfpy.Key.RIGHT: + try_move_or_attack(1, 0) + +scene.on_key = handle_keys + +# ============================================================================= +# Start the Game +# ============================================================================= + +scene.activate() +print("Part 7 loaded! Notice the improved UI with health bar and message log.") diff --git a/tests/snippets/278_part_08_items.py b/tests/snippets/278_part_08_items.py new file mode 100644 index 0000000..e3d3313 --- /dev/null +++ b/tests/snippets/278_part_08_items.py @@ -0,0 +1,1274 @@ +# mcrf: objects=[Caption,Color,ColorLayer,Entity,FOV,Frame,Grid,InputState,Key,Scene,Texture] verified=0.2.8-dev status=ok +"""McRogueFace Tutorial - Part 8: Items and Inventory + +Add health potions and inventory management. +""" +import mcrfpy +import random +from dataclasses import dataclass, field +from typing import Optional + +# ============================================================================= +# Constants +# ============================================================================= + +# Sprite indices for CP437 tileset +SPRITE_WALL = 35 # '#' - wall +SPRITE_FLOOR = 46 # '.' - floor +SPRITE_PLAYER = 64 # '@' - player +SPRITE_CORPSE = 37 # '%' - remains +SPRITE_POTION = 173 # Potion sprite (or use '!' = 33) + +# Enemy sprites +SPRITE_GOBLIN = 103 # 'g' +SPRITE_ORC = 111 # 'o' +SPRITE_TROLL = 116 # 't' + +# Grid dimensions +GRID_WIDTH = 50 +GRID_HEIGHT = 30 + +# Room generation parameters +ROOM_MIN_SIZE = 6 +ROOM_MAX_SIZE = 12 +MAX_ROOMS = 8 + +# Enemy spawn parameters +MAX_ENEMIES_PER_ROOM = 3 + +# Item spawn parameters +MAX_ITEMS_PER_ROOM = 2 + +# FOV settings +FOV_RADIUS = 8 + +# Visibility colors +COLOR_VISIBLE = mcrfpy.Color(0, 0, 0, 0) +COLOR_DISCOVERED = mcrfpy.Color(0, 0, 40, 180) +COLOR_UNKNOWN = mcrfpy.Color(0, 0, 0, 255) + +# Message colors +COLOR_PLAYER_ATTACK = mcrfpy.Color(200, 200, 200) +COLOR_ENEMY_ATTACK = mcrfpy.Color(255, 150, 150) +COLOR_PLAYER_DEATH = mcrfpy.Color(255, 50, 50) +COLOR_ENEMY_DEATH = mcrfpy.Color(100, 255, 100) +COLOR_HEAL = mcrfpy.Color(100, 255, 100) +COLOR_PICKUP = mcrfpy.Color(100, 200, 255) +COLOR_INFO = mcrfpy.Color(100, 100, 255) +COLOR_WARNING = mcrfpy.Color(255, 200, 50) +COLOR_INVALID = mcrfpy.Color(255, 100, 100) + +# UI Layout constants +UI_TOP_HEIGHT = 60 +UI_BOTTOM_HEIGHT = 150 +GAME_AREA_Y = UI_TOP_HEIGHT +GAME_AREA_HEIGHT = 768 - UI_TOP_HEIGHT - UI_BOTTOM_HEIGHT + +# ============================================================================= +# Fighter Component +# ============================================================================= + +@dataclass +class Fighter: + """Combat stats for an entity.""" + hp: int + max_hp: int + attack: int + defense: int + name: str + is_player: bool = False + + @property + def is_alive(self) -> bool: + return self.hp > 0 + + def take_damage(self, amount: int) -> int: + actual_damage = min(self.hp, amount) + self.hp -= actual_damage + return actual_damage + + def heal(self, amount: int) -> int: + actual_heal = min(self.max_hp - self.hp, amount) + self.hp += actual_heal + return actual_heal + +# ============================================================================= +# Item Component +# ============================================================================= + +@dataclass +class Item: + """Data for an item that can be picked up and used.""" + name: str + item_type: str + heal_amount: int = 0 + + def describe(self) -> str: + """Return a description of what this item does.""" + if self.item_type == "health_potion": + return f"Restores {self.heal_amount} HP" + return "Unknown item" + +# ============================================================================= +# Inventory System +# ============================================================================= + +@dataclass +class Inventory: + """Container for items the player is carrying.""" + capacity: int = 10 + items: list = field(default_factory=list) + + def add(self, item: Item) -> bool: + """Add an item to the inventory. + + Args: + item: The item to add + + Returns: + True if item was added, False if inventory is full + """ + if len(self.items) >= self.capacity: + return False + self.items.append(item) + return True + + def remove(self, index: int) -> Optional[Item]: + """Remove and return an item by index. + + Args: + index: The index of the item to remove + + Returns: + The removed item, or None if index is invalid + """ + if 0 <= index < len(self.items): + return self.items.pop(index) + return None + + def get(self, index: int) -> Optional[Item]: + """Get an item by index without removing it.""" + if 0 <= index < len(self.items): + return self.items[index] + return None + + def is_full(self) -> bool: + """Check if the inventory is full.""" + return len(self.items) >= self.capacity + + def count(self) -> int: + """Return the number of items in the inventory.""" + return len(self.items) + +# ============================================================================= +# Item Templates +# ============================================================================= + +ITEM_TEMPLATES = { + "health_potion": { + "name": "Health Potion", + "sprite": SPRITE_POTION, + "item_type": "health_potion", + "heal_amount": 10 + } +} + +# ============================================================================= +# Enemy Templates +# ============================================================================= + +ENEMY_TEMPLATES = { + "goblin": { + "sprite": SPRITE_GOBLIN, + "hp": 6, + "attack": 3, + "defense": 0 + }, + "orc": { + "sprite": SPRITE_ORC, + "hp": 10, + "attack": 4, + "defense": 1 + }, + "troll": { + "sprite": SPRITE_TROLL, + "hp": 16, + "attack": 6, + "defense": 2 + } +} + +# ============================================================================= +# Message Log System +# ============================================================================= + +class MessageLog: + """A message log that displays recent game messages with colors.""" + + def __init__(self, x: int, y: int, width: int, height: int, max_messages: int = 6): + self.x = x + self.y = y + self.width = width + self.height = height + self.max_messages = max_messages + self.messages: list[tuple[str, mcrfpy.Color]] = [] + self.captions: list[mcrfpy.Caption] = [] + + self.frame = mcrfpy.Frame( + pos=(x, y), + size=(width, height) + ) + self.frame.fill_color = mcrfpy.Color(20, 20, 30, 200) + self.frame.outline = 2 + self.frame.outline_color = mcrfpy.Color(80, 80, 100) + + line_height = 20 + for i in range(max_messages): + caption = mcrfpy.Caption( + pos=(x + 10, y + 5 + i * line_height), + text="" + ) + caption.font_size = 14 + caption.fill_color = mcrfpy.Color(200, 200, 200) + self.captions.append(caption) + + def add_to_scene(self, scene: mcrfpy.Scene) -> None: + scene.children.append(self.frame) + for caption in self.captions: + scene.children.append(caption) + + def add(self, text: str, color: mcrfpy.Color = None) -> None: + if color is None: + color = mcrfpy.Color(200, 200, 200) + + self.messages.append((text, color)) + + while len(self.messages) > self.max_messages: + self.messages.pop(0) + + self._refresh() + + def _refresh(self) -> None: + for i, caption in enumerate(self.captions): + if i < len(self.messages): + text, color = self.messages[i] + caption.text = text + caption.fill_color = color + else: + caption.text = "" + + def clear(self) -> None: + self.messages.clear() + self._refresh() + +# ============================================================================= +# Health Bar System +# ============================================================================= + +class HealthBar: + """A visual health bar using nested frames.""" + + def __init__(self, x: int, y: int, width: int, height: int): + self.x = x + self.y = y + self.width = width + self.height = height + self.max_hp = 30 + self.current_hp = 30 + + self.bg_frame = mcrfpy.Frame( + pos=(x, y), + size=(width, height) + ) + self.bg_frame.fill_color = mcrfpy.Color(80, 0, 0) + self.bg_frame.outline = 2 + self.bg_frame.outline_color = mcrfpy.Color(150, 150, 150) + + self.fg_frame = mcrfpy.Frame( + pos=(x + 2, y + 2), + size=(width - 4, height - 4) + ) + self.fg_frame.fill_color = mcrfpy.Color(0, 180, 0) + self.fg_frame.outline = 0 + + self.label = mcrfpy.Caption( + pos=(x + 5, y + 2), + text=f"HP: {self.current_hp}/{self.max_hp}" + ) + self.label.font_size = 16 + self.label.fill_color = mcrfpy.Color(255, 255, 255) + + def add_to_scene(self, scene: mcrfpy.Scene) -> None: + scene.children.append(self.bg_frame) + scene.children.append(self.fg_frame) + scene.children.append(self.label) + + def update(self, current_hp: int, max_hp: int) -> None: + self.current_hp = current_hp + self.max_hp = max_hp + + percent = max(0, current_hp / max_hp) if max_hp > 0 else 0 + + inner_width = self.width - 4 + self.fg_frame.resize(int(inner_width * percent), self.height - 4) + + self.label.text = f"HP: {current_hp}/{max_hp}" + + if percent > 0.6: + self.fg_frame.fill_color = mcrfpy.Color(0, 180, 0) + elif percent > 0.3: + self.fg_frame.fill_color = mcrfpy.Color(180, 180, 0) + else: + self.fg_frame.fill_color = mcrfpy.Color(180, 0, 0) + +# ============================================================================= +# Inventory Panel +# ============================================================================= + +class InventoryPanel: + """A panel displaying the player's inventory.""" + + def __init__(self, x: int, y: int, width: int, height: int): + self.x = x + self.y = y + self.width = width + self.height = height + self.captions: list[mcrfpy.Caption] = [] + + self.frame = mcrfpy.Frame( + pos=(x, y), + size=(width, height) + ) + self.frame.fill_color = mcrfpy.Color(20, 20, 30, 200) + self.frame.outline = 2 + self.frame.outline_color = mcrfpy.Color(80, 80, 100) + + self.title = mcrfpy.Caption( + pos=(x + 10, y + 5), + text="Inventory (G:pickup, U:use)" + ) + self.title.font_size = 14 + self.title.fill_color = mcrfpy.Color(200, 200, 255) + + # Create slots for inventory items + for i in range(5): # Show up to 5 items + caption = mcrfpy.Caption( + pos=(x + 10, y + 25 + i * 18), + text="" + ) + caption.font_size = 13 + caption.fill_color = mcrfpy.Color(180, 180, 180) + self.captions.append(caption) + + def add_to_scene(self, scene: mcrfpy.Scene) -> None: + scene.children.append(self.frame) + scene.children.append(self.title) + for caption in self.captions: + scene.children.append(caption) + + def update(self, inventory: Inventory) -> None: + """Update the inventory display.""" + for i, caption in enumerate(self.captions): + if i < len(inventory.items): + item = inventory.items[i] + caption.text = f"{i+1}. {item.name}" + caption.fill_color = mcrfpy.Color(180, 180, 180) + else: + caption.text = f"{i+1}. ---" + caption.fill_color = mcrfpy.Color(80, 80, 80) + +# ============================================================================= +# Global State +# ============================================================================= + +entity_data: dict[mcrfpy.Entity, Fighter] = {} +item_data: dict[mcrfpy.Entity, Item] = {} + +player: Optional[mcrfpy.Entity] = None +player_inventory: Optional[Inventory] = None +grid: Optional[mcrfpy.Grid] = None +fov_layer = None +texture: Optional[mcrfpy.Texture] = None +game_over: bool = False +dungeon_level: int = 1 + +# UI components +message_log: Optional[MessageLog] = None +health_bar: Optional[HealthBar] = None +inventory_panel: Optional[InventoryPanel] = None + +# ============================================================================= +# Room Class +# ============================================================================= + +class RectangularRoom: + """A rectangular room with its position and size.""" + + def __init__(self, x: int, y: int, width: int, height: int): + self.x1 = x + self.y1 = y + self.x2 = x + width + self.y2 = y + height + + @property + def center(self) -> tuple[int, int]: + center_x = (self.x1 + self.x2) // 2 + center_y = (self.y1 + self.y2) // 2 + return center_x, center_y + + @property + def inner(self) -> tuple[slice, slice]: + return slice(self.x1 + 1, self.x2), slice(self.y1 + 1, self.y2) + + def intersects(self, other: "RectangularRoom") -> bool: + return ( + self.x1 <= other.x2 and + self.x2 >= other.x1 and + self.y1 <= other.y2 and + self.y2 >= other.y1 + ) + +# ============================================================================= +# Exploration Tracking +# ============================================================================= + +explored: list[list[bool]] = [] + +def init_explored() -> None: + global explored + explored = [[False for _ in range(GRID_WIDTH)] for _ in range(GRID_HEIGHT)] + +def mark_explored(x: int, y: int) -> None: + if 0 <= x < GRID_WIDTH and 0 <= y < GRID_HEIGHT: + explored[y][x] = True + +def is_explored(x: int, y: int) -> bool: + if 0 <= x < GRID_WIDTH and 0 <= y < GRID_HEIGHT: + return explored[y][x] + return False + +# ============================================================================= +# Dungeon Generation +# ============================================================================= + +def fill_with_walls(target_grid: mcrfpy.Grid) -> None: + for y in range(GRID_HEIGHT): + for x in range(GRID_WIDTH): + cell = target_grid.at(x, y) + cell.tilesprite = SPRITE_WALL + cell.walkable = False + cell.transparent = False + +def carve_room(target_grid: mcrfpy.Grid, room: RectangularRoom) -> None: + inner_x, inner_y = room.inner + for y in range(inner_y.start, inner_y.stop): + for x in range(inner_x.start, inner_x.stop): + if 0 <= x < GRID_WIDTH and 0 <= y < GRID_HEIGHT: + cell = target_grid.at(x, y) + cell.tilesprite = SPRITE_FLOOR + cell.walkable = True + cell.transparent = True + +def carve_tunnel_horizontal(target_grid: mcrfpy.Grid, x1: int, x2: int, y: int) -> None: + for x in range(min(x1, x2), max(x1, x2) + 1): + if 0 <= x < GRID_WIDTH and 0 <= y < GRID_HEIGHT: + cell = target_grid.at(x, y) + cell.tilesprite = SPRITE_FLOOR + cell.walkable = True + cell.transparent = True + +def carve_tunnel_vertical(target_grid: mcrfpy.Grid, y1: int, y2: int, x: int) -> None: + for y in range(min(y1, y2), max(y1, y2) + 1): + if 0 <= x < GRID_WIDTH and 0 <= y < GRID_HEIGHT: + cell = target_grid.at(x, y) + cell.tilesprite = SPRITE_FLOOR + cell.walkable = True + cell.transparent = True + +def carve_l_tunnel( + target_grid: mcrfpy.Grid, + start: tuple[int, int], + end: tuple[int, int] +) -> None: + x1, y1 = start + x2, y2 = end + + if random.random() < 0.5: + carve_tunnel_horizontal(target_grid, x1, x2, y1) + carve_tunnel_vertical(target_grid, y1, y2, x2) + else: + carve_tunnel_vertical(target_grid, y1, y2, x1) + carve_tunnel_horizontal(target_grid, x1, x2, y2) + +# ============================================================================= +# Entity Management +# ============================================================================= + +def spawn_enemy(target_grid: mcrfpy.Grid, x: int, y: int, enemy_type: str, tex: mcrfpy.Texture) -> mcrfpy.Entity: + template = ENEMY_TEMPLATES[enemy_type] + + enemy = mcrfpy.Entity( + grid_pos=(x, y), + texture=tex, + sprite_index=template["sprite"] + ) + enemy.visible = False + + target_grid.entities.append(enemy) + + entity_data[enemy] = Fighter( + hp=template["hp"], + max_hp=template["hp"], + attack=template["attack"], + defense=template["defense"], + name=enemy_type.capitalize(), + is_player=False + ) + + return enemy + +def spawn_enemies_in_room(target_grid: mcrfpy.Grid, room: RectangularRoom, tex: mcrfpy.Texture) -> None: + num_enemies = random.randint(0, MAX_ENEMIES_PER_ROOM) + + for _ in range(num_enemies): + inner_x, inner_y = room.inner + x = random.randint(inner_x.start, inner_x.stop - 1) + y = random.randint(inner_y.start, inner_y.stop - 1) + + if is_position_occupied(target_grid, x, y): + continue + + roll = random.random() + if roll < 0.6: + enemy_type = "goblin" + elif roll < 0.9: + enemy_type = "orc" + else: + enemy_type = "troll" + + spawn_enemy(target_grid, x, y, enemy_type, tex) + +# ============================================================================= +# Item Management +# ============================================================================= + +def spawn_item(target_grid: mcrfpy.Grid, x: int, y: int, item_type: str, tex: mcrfpy.Texture) -> mcrfpy.Entity: + """Spawn an item at the given position. + + Args: + target_grid: The game grid + x: X position in tiles + y: Y position in tiles + item_type: Type of item to spawn + tex: The texture to use + + Returns: + The created item entity + """ + template = ITEM_TEMPLATES[item_type] + + item_entity = mcrfpy.Entity( + grid_pos=(x, y), + texture=tex, + sprite_index=template["sprite"] + ) + item_entity.visible = False + + target_grid.entities.append(item_entity) + + # Create Item data + item_data[item_entity] = Item( + name=template["name"], + item_type=template["item_type"], + heal_amount=template.get("heal_amount", 0) + ) + + return item_entity + +def spawn_items_in_room(target_grid: mcrfpy.Grid, room: RectangularRoom, tex: mcrfpy.Texture) -> None: + """Spawn random items in a room. + + Args: + target_grid: The game grid + room: The room to spawn items in + tex: The texture to use + """ + num_items = random.randint(0, MAX_ITEMS_PER_ROOM) + + for _ in range(num_items): + inner_x, inner_y = room.inner + x = random.randint(inner_x.start, inner_x.stop - 1) + y = random.randint(inner_y.start, inner_y.stop - 1) + + # Check if position is blocked by entity or item + if is_position_occupied(target_grid, x, y): + continue + + # For now, only spawn health potions + spawn_item(target_grid, x, y, "health_potion", tex) + +def get_item_at(target_grid: mcrfpy.Grid, x: int, y: int) -> Optional[mcrfpy.Entity]: + """Get an item entity at the given position. + + Args: + target_grid: The game grid + x: X position to check + y: Y position to check + + Returns: + The item entity, or None if no item at position + """ + for entity in target_grid.entities: + if entity in item_data: + if int(entity.grid_x) == x and int(entity.grid_y) == y: + return entity + return None + +def pickup_item() -> bool: + """Try to pick up an item at the player's position. + + Returns: + True if an item was picked up, False otherwise + """ + global player, player_inventory, grid + + px, py = int(player.grid_x), int(player.grid_y) + item_entity = get_item_at(grid, px, py) + + if item_entity is None: + message_log.add("There is nothing to pick up here.", COLOR_INVALID) + return False + + if player_inventory.is_full(): + message_log.add("Your inventory is full!", COLOR_WARNING) + return False + + # Get the item data + item = item_data.get(item_entity) + if item is None: + return False + + # Add to inventory + player_inventory.add(item) + + # Remove from ground + remove_item_entity(grid, item_entity) + + message_log.add(f"You pick up the {item.name}.", COLOR_PICKUP) + + update_ui() + return True + +def use_item(index: int) -> bool: + """Use an item from the inventory. + + Args: + index: The inventory index of the item to use + + Returns: + True if an item was used, False otherwise + """ + global player, player_inventory + + item = player_inventory.get(index) + if item is None: + message_log.add("Invalid item selection.", COLOR_INVALID) + return False + + # Handle different item types + if item.item_type == "health_potion": + fighter = entity_data.get(player) + if fighter is None: + return False + + if fighter.hp >= fighter.max_hp: + message_log.add("You are already at full health!", COLOR_WARNING) + return False + + # Apply healing + actual_heal = fighter.heal(item.heal_amount) + + # Remove the item from inventory + player_inventory.remove(index) + + message_log.add(f"You drink the {item.name} and recover {actual_heal} HP!", COLOR_HEAL) + + update_ui() + return True + + message_log.add(f"You cannot use the {item.name}.", COLOR_INVALID) + return False + +def remove_item_entity(target_grid: mcrfpy.Grid, entity: mcrfpy.Entity) -> None: + """Remove an item entity from the grid and item_data.""" + for i, e in enumerate(target_grid.entities): + if e == entity: + target_grid.entities.remove(entity) + break + + if entity in item_data: + del item_data[entity] + +# ============================================================================= +# Position Checking +# ============================================================================= + +def is_position_occupied(target_grid: mcrfpy.Grid, x: int, y: int) -> bool: + """Check if a position has any entity (enemy, item, or player). + + Args: + target_grid: The game grid + x: X position to check + y: Y position to check + + Returns: + True if position is occupied, False otherwise + """ + for entity in target_grid.entities: + if int(entity.grid_x) == x and int(entity.grid_y) == y: + return True + return False + +def get_blocking_entity_at(target_grid: mcrfpy.Grid, x: int, y: int, exclude: mcrfpy.Entity = None) -> Optional[mcrfpy.Entity]: + """Get any living entity that blocks movement at the given position.""" + for entity in target_grid.entities: + if entity == exclude: + continue + if int(entity.grid_x) == x and int(entity.grid_y) == y: + # Only fighters block movement + if entity in entity_data and entity_data[entity].is_alive: + return entity + return None + +def remove_entity(target_grid: mcrfpy.Grid, entity: mcrfpy.Entity) -> None: + for i, e in enumerate(target_grid.entities): + if e == entity: + target_grid.entities.remove(entity) + break + if entity in entity_data: + del entity_data[entity] + +def clear_all_entities(target_grid: mcrfpy.Grid) -> None: + """Remove all entities (enemies and items) except the player.""" + global entity_data, item_data + + entities_to_remove = [] + for entity in target_grid.entities: + if entity in entity_data and not entity_data[entity].is_player: + entities_to_remove.append(entity) + elif entity in item_data: + entities_to_remove.append(entity) + + for entity in entities_to_remove: + if entity in entity_data: + del entity_data[entity] + if entity in item_data: + del item_data[entity] + + for i, e in enumerate(target_grid.entities): + if e == entity: + target_grid.entities.remove(entity) + break + +# ============================================================================= +# Combat System +# ============================================================================= + +def calculate_damage(attacker: Fighter, defender: Fighter) -> int: + return max(0, attacker.attack - defender.defense) + +def perform_attack(attacker_entity: mcrfpy.Entity, defender_entity: mcrfpy.Entity) -> None: + global game_over + + attacker = entity_data.get(attacker_entity) + defender = entity_data.get(defender_entity) + + if attacker is None or defender is None: + return + + damage = calculate_damage(attacker, defender) + defender.take_damage(damage) + + if damage > 0: + if attacker.is_player: + message_log.add( + f"You hit the {defender.name} for {damage} damage!", + COLOR_PLAYER_ATTACK + ) + else: + message_log.add( + f"The {attacker.name} hits you for {damage} damage!", + COLOR_ENEMY_ATTACK + ) + else: + if attacker.is_player: + message_log.add( + f"You hit the {defender.name} but deal no damage.", + mcrfpy.Color(150, 150, 150) + ) + else: + message_log.add( + f"The {attacker.name} hits but deals no damage.", + mcrfpy.Color(150, 150, 200) + ) + + if not defender.is_alive: + handle_death(defender_entity, defender) + + update_ui() + +def handle_death(entity: mcrfpy.Entity, fighter: Fighter) -> None: + global game_over, grid + + if fighter.is_player: + message_log.add("You have died!", COLOR_PLAYER_DEATH) + message_log.add("Press R to restart or Escape to quit.", COLOR_INFO) + game_over = True + entity.sprite_index = SPRITE_CORPSE + else: + message_log.add(f"The {fighter.name} dies!", COLOR_ENEMY_DEATH) + remove_entity(grid, entity) + +# ============================================================================= +# Field of View +# ============================================================================= + +def update_entity_visibility(target_grid: mcrfpy.Grid) -> None: + global player + + for entity in target_grid.entities: + if entity == player: + entity.visible = True + continue + + ex, ey = int(entity.grid_x), int(entity.grid_y) + entity.visible = target_grid.is_in_fov(ex, ey) + +def update_fov(target_grid: mcrfpy.Grid, target_fov_layer, player_x: int, player_y: int) -> None: + target_grid.compute_fov((player_x, player_y), radius=FOV_RADIUS, algorithm=mcrfpy.FOV.SHADOW) + + for y in range(GRID_HEIGHT): + for x in range(GRID_WIDTH): + if target_grid.is_in_fov(x, y): + mark_explored(x, y) + target_fov_layer.set((x, y), COLOR_VISIBLE) + elif is_explored(x, y): + target_fov_layer.set((x, y), COLOR_DISCOVERED) + else: + target_fov_layer.set((x, y), COLOR_UNKNOWN) + + update_entity_visibility(target_grid) + +# ============================================================================= +# Movement and Actions +# ============================================================================= + +def can_move_to(target_grid: mcrfpy.Grid, x: int, y: int, mover: mcrfpy.Entity = None) -> bool: + if x < 0 or x >= GRID_WIDTH or y < 0 or y >= GRID_HEIGHT: + return False + + if not target_grid.at(x, y).walkable: + return False + + blocker = get_blocking_entity_at(target_grid, x, y, exclude=mover) + if blocker is not None: + return False + + return True + +def try_move_or_attack(dx: int, dy: int) -> None: + global player, grid, fov_layer, game_over + + if game_over: + return + + px, py = int(player.grid_x), int(player.grid_y) + target_x = px + dx + target_y = py + dy + + if target_x < 0 or target_x >= GRID_WIDTH or target_y < 0 or target_y >= GRID_HEIGHT: + return + + blocker = get_blocking_entity_at(grid, target_x, target_y, exclude=player) + + if blocker is not None: + perform_attack(player, blocker) + enemy_turn() + elif grid.at(target_x, target_y).walkable: + player.grid_x = target_x + player.grid_y = target_y + update_fov(grid, fov_layer, target_x, target_y) + enemy_turn() + + update_ui() + +# ============================================================================= +# Enemy AI +# ============================================================================= + +def enemy_turn() -> None: + global player, grid, game_over + + if game_over: + return + + player_x, player_y = int(player.grid_x), int(player.grid_y) + + enemies = [] + for entity in grid.entities: + if entity == player: + continue + if entity in entity_data and entity_data[entity].is_alive: + enemies.append(entity) + + for enemy in enemies: + fighter = entity_data.get(enemy) + if fighter is None or not fighter.is_alive: + continue + + ex, ey = int(enemy.grid_x), int(enemy.grid_y) + + if not grid.is_in_fov(ex, ey): + continue + + dx = player_x - ex + dy = player_y - ey + + if abs(dx) <= 1 and abs(dy) <= 1 and (dx != 0 or dy != 0): + perform_attack(enemy, player) + else: + move_toward_player(enemy, ex, ey, player_x, player_y) + +def move_toward_player(enemy: mcrfpy.Entity, ex: int, ey: int, px: int, py: int) -> None: + global grid + + dx = 0 + dy = 0 + + if px < ex: + dx = -1 + elif px > ex: + dx = 1 + + if py < ey: + dy = -1 + elif py > ey: + dy = 1 + + new_x = ex + dx + new_y = ey + dy + + if can_move_to(grid, new_x, new_y, enemy): + enemy.grid_x = new_x + enemy.grid_y = new_y + elif dx != 0 and can_move_to(grid, ex + dx, ey, enemy): + enemy.grid_x = ex + dx + elif dy != 0 and can_move_to(grid, ex, ey + dy, enemy): + enemy.grid_y = ey + dy + +# ============================================================================= +# UI Updates +# ============================================================================= + +def update_ui() -> None: + """Update all UI components.""" + global player, health_bar, inventory_panel, player_inventory + + if player in entity_data: + fighter = entity_data[player] + health_bar.update(fighter.hp, fighter.max_hp) + + if player_inventory: + inventory_panel.update(player_inventory) + +# ============================================================================= +# Game Setup +# ============================================================================= + +# Create the scene +scene = mcrfpy.Scene("game") + +# Load texture +texture = mcrfpy.Texture("assets/kenney_tinydungeon.png", 16, 16) + +# Create the grid +grid = mcrfpy.Grid( + pos=(20, GAME_AREA_Y), + size=(700, GAME_AREA_HEIGHT - 20), + grid_size=(GRID_WIDTH, GRID_HEIGHT), + texture=texture, + zoom=1.0 +) + +# Generate initial dungeon +fill_with_walls(grid) +init_explored() + +rooms: list[RectangularRoom] = [] + +for _ in range(MAX_ROOMS): + room_width = random.randint(ROOM_MIN_SIZE, ROOM_MAX_SIZE) + room_height = random.randint(ROOM_MIN_SIZE, ROOM_MAX_SIZE) + x = random.randint(1, GRID_WIDTH - room_width - 2) + y = random.randint(1, GRID_HEIGHT - room_height - 2) + + new_room = RectangularRoom(x, y, room_width, room_height) + + overlaps = False + for other_room in rooms: + if new_room.intersects(other_room): + overlaps = True + break + + if overlaps: + continue + + carve_room(grid, new_room) + + if rooms: + carve_l_tunnel(grid, new_room.center, rooms[-1].center) + + rooms.append(new_room) + +# Get player starting position +if rooms: + player_start_x, player_start_y = rooms[0].center +else: + player_start_x, player_start_y = GRID_WIDTH // 2, GRID_HEIGHT // 2 + +# Add FOV layer +fov_layer = mcrfpy.ColorLayer(name="fov", z_index=-1) +grid.add_layer(fov_layer) +for y in range(GRID_HEIGHT): + for x in range(GRID_WIDTH): + fov_layer.set((x, y), COLOR_UNKNOWN) + +# Create the player +player = mcrfpy.Entity( + grid_pos=(player_start_x, player_start_y), + texture=texture, + sprite_index=SPRITE_PLAYER +) +grid.entities.append(player) + +entity_data[player] = Fighter( + hp=30, + max_hp=30, + attack=5, + defense=2, + name="Player", + is_player=True +) + +# Create player inventory +player_inventory = Inventory(capacity=10) + +# Spawn enemies and items +for i, room in enumerate(rooms): + if i == 0: + continue # Skip player's starting room + spawn_enemies_in_room(grid, room, texture) + spawn_items_in_room(grid, room, texture) + +# Calculate initial FOV +update_fov(grid, fov_layer, player_start_x, player_start_y) + +# Add grid to scene +scene.children.append(grid) + +# ============================================================================= +# Create UI Elements +# ============================================================================= + +# Title bar +title = mcrfpy.Caption( + pos=(20, 10), + text="Part 8: Items and Inventory" +) +title.fill_color = mcrfpy.Color(255, 255, 255) +title.font_size = 24 +scene.children.append(title) + +# Instructions +instructions = mcrfpy.Caption( + pos=(300, 15), + text="WASD: Move | G: Pickup | 1-5: Use item | R: Restart" +) +instructions.fill_color = mcrfpy.Color(180, 180, 180) +instructions.font_size = 14 +scene.children.append(instructions) + +# Health Bar +health_bar = HealthBar( + x=730, + y=10, + width=280, + height=30 +) +health_bar.add_to_scene(scene) + +# Inventory Panel +inventory_panel = InventoryPanel( + x=730, + y=GAME_AREA_Y, + width=280, + height=150 +) +inventory_panel.add_to_scene(scene) + +# Message Log +message_log = MessageLog( + x=20, + y=768 - UI_BOTTOM_HEIGHT + 10, + width=990, + height=UI_BOTTOM_HEIGHT - 20, + max_messages=6 +) +message_log.add_to_scene(scene) + +# Initial messages +message_log.add("Welcome to the dungeon!", COLOR_INFO) +message_log.add("Find potions to heal. Press G to pick up items.", COLOR_INFO) + +# Initialize UI +update_ui() + +# ============================================================================= +# Input Handling +# ============================================================================= + +def restart_game() -> None: + """Restart the game with a new dungeon.""" + global player, grid, fov_layer, game_over, entity_data, item_data, rooms + global player_inventory + + game_over = False + + entity_data.clear() + item_data.clear() + + while len(grid.entities) > 0: + grid.entities.pop() + + fill_with_walls(grid) + init_explored() + message_log.clear() + + rooms = [] + + for _ in range(MAX_ROOMS): + room_width = random.randint(ROOM_MIN_SIZE, ROOM_MAX_SIZE) + room_height = random.randint(ROOM_MIN_SIZE, ROOM_MAX_SIZE) + x = random.randint(1, GRID_WIDTH - room_width - 2) + y = random.randint(1, GRID_HEIGHT - room_height - 2) + + new_room = RectangularRoom(x, y, room_width, room_height) + + overlaps = False + for other_room in rooms: + if new_room.intersects(other_room): + overlaps = True + break + + if overlaps: + continue + + carve_room(grid, new_room) + + if rooms: + carve_l_tunnel(grid, new_room.center, rooms[-1].center) + + rooms.append(new_room) + + if rooms: + new_x, new_y = rooms[0].center + else: + new_x, new_y = GRID_WIDTH // 2, GRID_HEIGHT // 2 + + player = mcrfpy.Entity( + grid_pos=(new_x, new_y), + texture=texture, + sprite_index=SPRITE_PLAYER + ) + grid.entities.append(player) + + entity_data[player] = Fighter( + hp=30, + max_hp=30, + attack=5, + defense=2, + name="Player", + is_player=True + ) + + # Reset inventory + player_inventory = Inventory(capacity=10) + + for i, room in enumerate(rooms): + if i == 0: + continue + spawn_enemies_in_room(grid, room, texture) + spawn_items_in_room(grid, room, texture) + + for y in range(GRID_HEIGHT): + for x in range(GRID_WIDTH): + fov_layer.set((x, y), COLOR_UNKNOWN) + + update_fov(grid, fov_layer, new_x, new_y) + + message_log.add("A new adventure begins!", COLOR_INFO) + + update_ui() + +NUMBER_KEYS = [ + mcrfpy.Key.NUM_1, + mcrfpy.Key.NUM_2, + mcrfpy.Key.NUM_3, + mcrfpy.Key.NUM_4, + mcrfpy.Key.NUM_5, +] + +def handle_keys(key: mcrfpy.Key, action: mcrfpy.InputState) -> None: + global game_over + + if action != mcrfpy.InputState.PRESSED: + return + + if key == mcrfpy.Key.R: + restart_game() + return + + if key == mcrfpy.Key.ESCAPE: + mcrfpy.exit() + return + + if game_over: + return + + # Movement + if key == mcrfpy.Key.W or key == mcrfpy.Key.UP: + try_move_or_attack(0, -1) + elif key == mcrfpy.Key.S or key == mcrfpy.Key.DOWN: + try_move_or_attack(0, 1) + elif key == mcrfpy.Key.A or key == mcrfpy.Key.LEFT: + try_move_or_attack(-1, 0) + elif key == mcrfpy.Key.D or key == mcrfpy.Key.RIGHT: + try_move_or_attack(1, 0) + # Pickup + elif key == mcrfpy.Key.G or key == mcrfpy.Key.COMMA: + pickup_item() + # Use items by number key + elif key in NUMBER_KEYS: + index = NUMBER_KEYS.index(key) + if use_item(index): + enemy_turn() # Using an item takes a turn + update_ui() + +scene.on_key = handle_keys + +# ============================================================================= +# Start the Game +# ============================================================================= + +scene.activate() +print("Part 8 loaded! Pick up health potions with G, use with 1-5.") diff --git a/tests/snippets/279_part_09_ranged.py b/tests/snippets/279_part_09_ranged.py new file mode 100644 index 0000000..0981018 --- /dev/null +++ b/tests/snippets/279_part_09_ranged.py @@ -0,0 +1,1395 @@ +# mcrf: objects=[Caption,Color,ColorLayer,Entity,FOV,Frame,Grid,InputState,Key,Scene,Texture] verified=0.2.8-dev status=ok +"""McRogueFace Tutorial - Part 9: Ranged Combat and Targeting + +Add ranged attacks with a targeting system. +""" +import mcrfpy +import random +from dataclasses import dataclass, field +from typing import Optional +from enum import Enum + +# ============================================================================= +# Constants +# ============================================================================= + +# Sprite indices for CP437 tileset +SPRITE_WALL = 35 # '#' - wall +SPRITE_FLOOR = 46 # '.' - floor +SPRITE_PLAYER = 64 # '@' - player +SPRITE_CORPSE = 37 # '%' - remains +SPRITE_POTION = 173 # Potion sprite +SPRITE_CURSOR = 88 # 'X' - targeting cursor + +# Enemy sprites +SPRITE_GOBLIN = 103 # 'g' +SPRITE_ORC = 111 # 'o' +SPRITE_TROLL = 116 # 't' + +# Grid dimensions +GRID_WIDTH = 50 +GRID_HEIGHT = 30 + +# Room generation parameters +ROOM_MIN_SIZE = 6 +ROOM_MAX_SIZE = 12 +MAX_ROOMS = 8 + +# Enemy spawn parameters +MAX_ENEMIES_PER_ROOM = 3 + +# Item spawn parameters +MAX_ITEMS_PER_ROOM = 2 + +# FOV and targeting settings +FOV_RADIUS = 8 +RANGED_ATTACK_RANGE = 6 +RANGED_ATTACK_DAMAGE = 4 + +# Visibility colors +COLOR_VISIBLE = mcrfpy.Color(0, 0, 0, 0) +COLOR_DISCOVERED = mcrfpy.Color(0, 0, 40, 180) +COLOR_UNKNOWN = mcrfpy.Color(0, 0, 0, 255) + +# Message colors +COLOR_PLAYER_ATTACK = mcrfpy.Color(200, 200, 200) +COLOR_ENEMY_ATTACK = mcrfpy.Color(255, 150, 150) +COLOR_PLAYER_DEATH = mcrfpy.Color(255, 50, 50) +COLOR_ENEMY_DEATH = mcrfpy.Color(100, 255, 100) +COLOR_HEAL = mcrfpy.Color(100, 255, 100) +COLOR_PICKUP = mcrfpy.Color(100, 200, 255) +COLOR_INFO = mcrfpy.Color(100, 100, 255) +COLOR_WARNING = mcrfpy.Color(255, 200, 50) +COLOR_INVALID = mcrfpy.Color(255, 100, 100) +COLOR_RANGED = mcrfpy.Color(255, 255, 100) + +# UI Layout constants +UI_TOP_HEIGHT = 60 +UI_BOTTOM_HEIGHT = 150 +GAME_AREA_Y = UI_TOP_HEIGHT +GAME_AREA_HEIGHT = 768 - UI_TOP_HEIGHT - UI_BOTTOM_HEIGHT + +# ============================================================================= +# Game Modes +# ============================================================================= + +class GameMode(Enum): + """The current game input mode.""" + NORMAL = "normal" + TARGETING = "targeting" + +# ============================================================================= +# Fighter Component +# ============================================================================= + +@dataclass +class Fighter: + """Combat stats for an entity.""" + hp: int + max_hp: int + attack: int + defense: int + name: str + is_player: bool = False + + @property + def is_alive(self) -> bool: + return self.hp > 0 + + def take_damage(self, amount: int) -> int: + actual_damage = min(self.hp, amount) + self.hp -= actual_damage + return actual_damage + + def heal(self, amount: int) -> int: + actual_heal = min(self.max_hp - self.hp, amount) + self.hp += actual_heal + return actual_heal + +# ============================================================================= +# Item Component +# ============================================================================= + +@dataclass +class Item: + """Data for an item that can be picked up and used.""" + name: str + item_type: str + heal_amount: int = 0 + + def describe(self) -> str: + if self.item_type == "health_potion": + return f"Restores {self.heal_amount} HP" + return "Unknown item" + +# ============================================================================= +# Inventory System +# ============================================================================= + +@dataclass +class Inventory: + """Container for items the player is carrying.""" + capacity: int = 10 + items: list = field(default_factory=list) + + def add(self, item: Item) -> bool: + if len(self.items) >= self.capacity: + return False + self.items.append(item) + return True + + def remove(self, index: int) -> Optional[Item]: + if 0 <= index < len(self.items): + return self.items.pop(index) + return None + + def get(self, index: int) -> Optional[Item]: + if 0 <= index < len(self.items): + return self.items[index] + return None + + def is_full(self) -> bool: + return len(self.items) >= self.capacity + + def count(self) -> int: + return len(self.items) + +# ============================================================================= +# Templates +# ============================================================================= + +ITEM_TEMPLATES = { + "health_potion": { + "name": "Health Potion", + "sprite": SPRITE_POTION, + "item_type": "health_potion", + "heal_amount": 10 + } +} + +ENEMY_TEMPLATES = { + "goblin": { + "sprite": SPRITE_GOBLIN, + "hp": 6, + "attack": 3, + "defense": 0 + }, + "orc": { + "sprite": SPRITE_ORC, + "hp": 10, + "attack": 4, + "defense": 1 + }, + "troll": { + "sprite": SPRITE_TROLL, + "hp": 16, + "attack": 6, + "defense": 2 + } +} + +# ============================================================================= +# Message Log System +# ============================================================================= + +class MessageLog: + """A message log that displays recent game messages with colors.""" + + def __init__(self, x: int, y: int, width: int, height: int, max_messages: int = 6): + self.x = x + self.y = y + self.width = width + self.height = height + self.max_messages = max_messages + self.messages: list[tuple[str, mcrfpy.Color]] = [] + self.captions: list[mcrfpy.Caption] = [] + + self.frame = mcrfpy.Frame( + pos=(x, y), + size=(width, height) + ) + self.frame.fill_color = mcrfpy.Color(20, 20, 30, 200) + self.frame.outline = 2 + self.frame.outline_color = mcrfpy.Color(80, 80, 100) + + line_height = 20 + for i in range(max_messages): + caption = mcrfpy.Caption( + pos=(x + 10, y + 5 + i * line_height), + text="" + ) + caption.font_size = 14 + caption.fill_color = mcrfpy.Color(200, 200, 200) + self.captions.append(caption) + + def add_to_scene(self, scene: mcrfpy.Scene) -> None: + scene.children.append(self.frame) + for caption in self.captions: + scene.children.append(caption) + + def add(self, text: str, color: mcrfpy.Color = None) -> None: + if color is None: + color = mcrfpy.Color(200, 200, 200) + + self.messages.append((text, color)) + + while len(self.messages) > self.max_messages: + self.messages.pop(0) + + self._refresh() + + def _refresh(self) -> None: + for i, caption in enumerate(self.captions): + if i < len(self.messages): + text, color = self.messages[i] + caption.text = text + caption.fill_color = color + else: + caption.text = "" + + def clear(self) -> None: + self.messages.clear() + self._refresh() + +# ============================================================================= +# Health Bar System +# ============================================================================= + +class HealthBar: + """A visual health bar using nested frames.""" + + def __init__(self, x: int, y: int, width: int, height: int): + self.x = x + self.y = y + self.width = width + self.height = height + self.max_hp = 30 + self.current_hp = 30 + + self.bg_frame = mcrfpy.Frame( + pos=(x, y), + size=(width, height) + ) + self.bg_frame.fill_color = mcrfpy.Color(80, 0, 0) + self.bg_frame.outline = 2 + self.bg_frame.outline_color = mcrfpy.Color(150, 150, 150) + + self.fg_frame = mcrfpy.Frame( + pos=(x + 2, y + 2), + size=(width - 4, height - 4) + ) + self.fg_frame.fill_color = mcrfpy.Color(0, 180, 0) + self.fg_frame.outline = 0 + + self.label = mcrfpy.Caption( + pos=(x + 5, y + 2), + text=f"HP: {self.current_hp}/{self.max_hp}" + ) + self.label.font_size = 16 + self.label.fill_color = mcrfpy.Color(255, 255, 255) + + def add_to_scene(self, scene: mcrfpy.Scene) -> None: + scene.children.append(self.bg_frame) + scene.children.append(self.fg_frame) + scene.children.append(self.label) + + def update(self, current_hp: int, max_hp: int) -> None: + self.current_hp = current_hp + self.max_hp = max_hp + + percent = max(0, current_hp / max_hp) if max_hp > 0 else 0 + + inner_width = self.width - 4 + self.fg_frame.resize(int(inner_width * percent), self.height - 4) + + self.label.text = f"HP: {current_hp}/{max_hp}" + + if percent > 0.6: + self.fg_frame.fill_color = mcrfpy.Color(0, 180, 0) + elif percent > 0.3: + self.fg_frame.fill_color = mcrfpy.Color(180, 180, 0) + else: + self.fg_frame.fill_color = mcrfpy.Color(180, 0, 0) + +# ============================================================================= +# Inventory Panel +# ============================================================================= + +class InventoryPanel: + """A panel displaying the player's inventory.""" + + def __init__(self, x: int, y: int, width: int, height: int): + self.x = x + self.y = y + self.width = width + self.height = height + self.captions: list[mcrfpy.Caption] = [] + + self.frame = mcrfpy.Frame( + pos=(x, y), + size=(width, height) + ) + self.frame.fill_color = mcrfpy.Color(20, 20, 30, 200) + self.frame.outline = 2 + self.frame.outline_color = mcrfpy.Color(80, 80, 100) + + self.title = mcrfpy.Caption( + pos=(x + 10, y + 5), + text="Inventory (G:pickup, 1-5:use)" + ) + self.title.font_size = 14 + self.title.fill_color = mcrfpy.Color(200, 200, 255) + + for i in range(5): + caption = mcrfpy.Caption( + pos=(x + 10, y + 25 + i * 18), + text="" + ) + caption.font_size = 13 + caption.fill_color = mcrfpy.Color(180, 180, 180) + self.captions.append(caption) + + def add_to_scene(self, scene: mcrfpy.Scene) -> None: + scene.children.append(self.frame) + scene.children.append(self.title) + for caption in self.captions: + scene.children.append(caption) + + def update(self, inventory: Inventory) -> None: + for i, caption in enumerate(self.captions): + if i < len(inventory.items): + item = inventory.items[i] + caption.text = f"{i+1}. {item.name}" + caption.fill_color = mcrfpy.Color(180, 180, 180) + else: + caption.text = f"{i+1}. ---" + caption.fill_color = mcrfpy.Color(80, 80, 80) + +# ============================================================================= +# Mode Display +# ============================================================================= + +class ModeDisplay: + """Displays the current game mode.""" + + def __init__(self, x: int, y: int): + self.caption = mcrfpy.Caption( + pos=(x, y), + text="[NORMAL MODE]" + ) + self.caption.font_size = 16 + self.caption.fill_color = mcrfpy.Color(100, 255, 100) + + def add_to_scene(self, scene: mcrfpy.Scene) -> None: + scene.children.append(self.caption) + + def update(self, mode: GameMode) -> None: + if mode == GameMode.NORMAL: + self.caption.text = "[NORMAL MODE] - F: Ranged attack" + self.caption.fill_color = mcrfpy.Color(100, 255, 100) + elif mode == GameMode.TARGETING: + self.caption.text = "[TARGETING] - Arrows: Move, Enter: Fire, Esc: Cancel" + self.caption.fill_color = mcrfpy.Color(255, 255, 100) + +# ============================================================================= +# Global State +# ============================================================================= + +entity_data: dict[mcrfpy.Entity, Fighter] = {} +item_data: dict[mcrfpy.Entity, Item] = {} + +player: Optional[mcrfpy.Entity] = None +player_inventory: Optional[Inventory] = None +grid: Optional[mcrfpy.Grid] = None +fov_layer = None +texture: Optional[mcrfpy.Texture] = None +game_over: bool = False +dungeon_level: int = 1 + +# Game mode state +game_mode: GameMode = GameMode.NORMAL +target_cursor: Optional[mcrfpy.Entity] = None +target_x: int = 0 +target_y: int = 0 + +# UI components +message_log: Optional[MessageLog] = None +health_bar: Optional[HealthBar] = None +inventory_panel: Optional[InventoryPanel] = None +mode_display: Optional[ModeDisplay] = None + +# ============================================================================= +# Room Class +# ============================================================================= + +class RectangularRoom: + """A rectangular room with its position and size.""" + + def __init__(self, x: int, y: int, width: int, height: int): + self.x1 = x + self.y1 = y + self.x2 = x + width + self.y2 = y + height + + @property + def center(self) -> tuple[int, int]: + center_x = (self.x1 + self.x2) // 2 + center_y = (self.y1 + self.y2) // 2 + return center_x, center_y + + @property + def inner(self) -> tuple[slice, slice]: + return slice(self.x1 + 1, self.x2), slice(self.y1 + 1, self.y2) + + def intersects(self, other: "RectangularRoom") -> bool: + return ( + self.x1 <= other.x2 and + self.x2 >= other.x1 and + self.y1 <= other.y2 and + self.y2 >= other.y1 + ) + +# ============================================================================= +# Exploration Tracking +# ============================================================================= + +explored: list[list[bool]] = [] + +def init_explored() -> None: + global explored + explored = [[False for _ in range(GRID_WIDTH)] for _ in range(GRID_HEIGHT)] + +def mark_explored(x: int, y: int) -> None: + if 0 <= x < GRID_WIDTH and 0 <= y < GRID_HEIGHT: + explored[y][x] = True + +def is_explored(x: int, y: int) -> bool: + if 0 <= x < GRID_WIDTH and 0 <= y < GRID_HEIGHT: + return explored[y][x] + return False + +# ============================================================================= +# Dungeon Generation +# ============================================================================= + +def fill_with_walls(target_grid: mcrfpy.Grid) -> None: + for y in range(GRID_HEIGHT): + for x in range(GRID_WIDTH): + cell = target_grid.at(x, y) + cell.tilesprite = SPRITE_WALL + cell.walkable = False + cell.transparent = False + +def carve_room(target_grid: mcrfpy.Grid, room: RectangularRoom) -> None: + inner_x, inner_y = room.inner + for y in range(inner_y.start, inner_y.stop): + for x in range(inner_x.start, inner_x.stop): + if 0 <= x < GRID_WIDTH and 0 <= y < GRID_HEIGHT: + cell = target_grid.at(x, y) + cell.tilesprite = SPRITE_FLOOR + cell.walkable = True + cell.transparent = True + +def carve_tunnel_horizontal(target_grid: mcrfpy.Grid, x1: int, x2: int, y: int) -> None: + for x in range(min(x1, x2), max(x1, x2) + 1): + if 0 <= x < GRID_WIDTH and 0 <= y < GRID_HEIGHT: + cell = target_grid.at(x, y) + cell.tilesprite = SPRITE_FLOOR + cell.walkable = True + cell.transparent = True + +def carve_tunnel_vertical(target_grid: mcrfpy.Grid, y1: int, y2: int, x: int) -> None: + for y in range(min(y1, y2), max(y1, y2) + 1): + if 0 <= x < GRID_WIDTH and 0 <= y < GRID_HEIGHT: + cell = target_grid.at(x, y) + cell.tilesprite = SPRITE_FLOOR + cell.walkable = True + cell.transparent = True + +def carve_l_tunnel( + target_grid: mcrfpy.Grid, + start: tuple[int, int], + end: tuple[int, int] +) -> None: + x1, y1 = start + x2, y2 = end + + if random.random() < 0.5: + carve_tunnel_horizontal(target_grid, x1, x2, y1) + carve_tunnel_vertical(target_grid, y1, y2, x2) + else: + carve_tunnel_vertical(target_grid, y1, y2, x1) + carve_tunnel_horizontal(target_grid, x1, x2, y2) + +# ============================================================================= +# Entity Management +# ============================================================================= + +def spawn_enemy(target_grid: mcrfpy.Grid, x: int, y: int, enemy_type: str, tex: mcrfpy.Texture) -> mcrfpy.Entity: + template = ENEMY_TEMPLATES[enemy_type] + + enemy = mcrfpy.Entity( + grid_pos=(x, y), + texture=tex, + sprite_index=template["sprite"] + ) + enemy.visible = False + + target_grid.entities.append(enemy) + + entity_data[enemy] = Fighter( + hp=template["hp"], + max_hp=template["hp"], + attack=template["attack"], + defense=template["defense"], + name=enemy_type.capitalize(), + is_player=False + ) + + return enemy + +def spawn_enemies_in_room(target_grid: mcrfpy.Grid, room: RectangularRoom, tex: mcrfpy.Texture) -> None: + num_enemies = random.randint(0, MAX_ENEMIES_PER_ROOM) + + for _ in range(num_enemies): + inner_x, inner_y = room.inner + x = random.randint(inner_x.start, inner_x.stop - 1) + y = random.randint(inner_y.start, inner_y.stop - 1) + + if is_position_occupied(target_grid, x, y): + continue + + roll = random.random() + if roll < 0.6: + enemy_type = "goblin" + elif roll < 0.9: + enemy_type = "orc" + else: + enemy_type = "troll" + + spawn_enemy(target_grid, x, y, enemy_type, tex) + +def spawn_item(target_grid: mcrfpy.Grid, x: int, y: int, item_type: str, tex: mcrfpy.Texture) -> mcrfpy.Entity: + template = ITEM_TEMPLATES[item_type] + + item_entity = mcrfpy.Entity( + grid_pos=(x, y), + texture=tex, + sprite_index=template["sprite"] + ) + item_entity.visible = False + + target_grid.entities.append(item_entity) + + item_data[item_entity] = Item( + name=template["name"], + item_type=template["item_type"], + heal_amount=template.get("heal_amount", 0) + ) + + return item_entity + +def spawn_items_in_room(target_grid: mcrfpy.Grid, room: RectangularRoom, tex: mcrfpy.Texture) -> None: + num_items = random.randint(0, MAX_ITEMS_PER_ROOM) + + for _ in range(num_items): + inner_x, inner_y = room.inner + x = random.randint(inner_x.start, inner_x.stop - 1) + y = random.randint(inner_y.start, inner_y.stop - 1) + + if is_position_occupied(target_grid, x, y): + continue + + spawn_item(target_grid, x, y, "health_potion", tex) + +def is_position_occupied(target_grid: mcrfpy.Grid, x: int, y: int) -> bool: + for entity in target_grid.entities: + if int(entity.grid_x) == x and int(entity.grid_y) == y: + return True + return False + +def get_item_at(target_grid: mcrfpy.Grid, x: int, y: int) -> Optional[mcrfpy.Entity]: + for entity in target_grid.entities: + if entity in item_data: + if int(entity.grid_x) == x and int(entity.grid_y) == y: + return entity + return None + +def get_blocking_entity_at(target_grid: mcrfpy.Grid, x: int, y: int, exclude: mcrfpy.Entity = None) -> Optional[mcrfpy.Entity]: + for entity in target_grid.entities: + if entity == exclude: + continue + if int(entity.grid_x) == x and int(entity.grid_y) == y: + if entity in entity_data and entity_data[entity].is_alive: + return entity + return None + +def remove_entity(target_grid: mcrfpy.Grid, entity: mcrfpy.Entity) -> None: + for i, e in enumerate(target_grid.entities): + if e == entity: + target_grid.entities.remove(entity) + break + if entity in entity_data: + del entity_data[entity] + +def remove_item_entity(target_grid: mcrfpy.Grid, entity: mcrfpy.Entity) -> None: + for i, e in enumerate(target_grid.entities): + if e == entity: + target_grid.entities.remove(entity) + break + if entity in item_data: + del item_data[entity] + +def clear_all_entities(target_grid: mcrfpy.Grid) -> None: + global entity_data, item_data + + entities_to_remove = [] + for entity in target_grid.entities: + if entity in entity_data and not entity_data[entity].is_player: + entities_to_remove.append(entity) + elif entity in item_data: + entities_to_remove.append(entity) + + for entity in entities_to_remove: + if entity in entity_data: + del entity_data[entity] + if entity in item_data: + del item_data[entity] + + for i, e in enumerate(target_grid.entities): + if e == entity: + target_grid.entities.remove(entity) + break + +# ============================================================================= +# Targeting System +# ============================================================================= + +def enter_targeting_mode() -> None: + """Enter targeting mode for ranged attack.""" + global game_mode, target_cursor, target_x, target_y, player, grid, texture + + # Start at player position + target_x = int(player.grid_x) + target_y = int(player.grid_y) + + # Create the targeting cursor + target_cursor = mcrfpy.Entity( + grid_pos=(target_x, target_y), + texture=texture, + sprite_index=SPRITE_CURSOR + ) + grid.entities.append(target_cursor) + + game_mode = GameMode.TARGETING + + message_log.add("Targeting mode: Use arrows to aim, Enter to fire, Esc to cancel.", COLOR_INFO) + mode_display.update(game_mode) + +def exit_targeting_mode() -> None: + """Exit targeting mode without firing.""" + global game_mode, target_cursor, grid + + if target_cursor is not None: + # Remove cursor from grid + for i, e in enumerate(grid.entities): + if e == target_cursor: + grid.entities.remove(target_cursor) + break + target_cursor = None + + game_mode = GameMode.NORMAL + mode_display.update(game_mode) + +def move_cursor(dx: int, dy: int) -> None: + """Move the targeting cursor.""" + global target_x, target_y, target_cursor, grid, player + + new_x = target_x + dx + new_y = target_y + dy + + # Check bounds + if new_x < 0 or new_x >= GRID_WIDTH or new_y < 0 or new_y >= GRID_HEIGHT: + return + + # Check if position is in FOV (can only target visible tiles) + if not grid.is_in_fov(new_x, new_y): + message_log.add("You cannot see that location.", COLOR_INVALID) + return + + # Check range + player_x, player_y = int(player.grid_x), int(player.grid_y) + distance = abs(new_x - player_x) + abs(new_y - player_y) + if distance > RANGED_ATTACK_RANGE: + message_log.add(f"Target is out of range (max {RANGED_ATTACK_RANGE}).", COLOR_WARNING) + return + + # Move cursor + target_x = new_x + target_y = new_y + target_cursor.grid_x = target_x + target_cursor.grid_y = target_y + + # Show info about target location + enemy = get_blocking_entity_at(grid, target_x, target_y, exclude=player) + if enemy and enemy in entity_data: + fighter = entity_data[enemy] + message_log.add(f"Target: {fighter.name} (HP: {fighter.hp}/{fighter.max_hp})", COLOR_RANGED) + +def confirm_target() -> None: + """Confirm the target and execute ranged attack.""" + global game_mode, target_x, target_y, player, grid + + # Check if targeting self + if target_x == int(player.grid_x) and target_y == int(player.grid_y): + message_log.add("You cannot target yourself!", COLOR_INVALID) + return + + # Check for enemy at target + target_enemy = get_blocking_entity_at(grid, target_x, target_y, exclude=player) + + if target_enemy is None or target_enemy not in entity_data: + message_log.add("No valid target at that location.", COLOR_INVALID) + return + + # Perform ranged attack + perform_ranged_attack(target_enemy) + + # Exit targeting mode + exit_targeting_mode() + + # Enemies take their turn + enemy_turn() + + update_ui() + +def perform_ranged_attack(target_entity: mcrfpy.Entity) -> None: + """Execute a ranged attack on the target. + + Args: + target_entity: The entity to attack + """ + global player, game_over + + defender = entity_data.get(target_entity) + attacker = entity_data.get(player) + + if defender is None or attacker is None: + return + + # Ranged attacks deal fixed damage (ignores defense partially) + damage = max(1, RANGED_ATTACK_DAMAGE - defender.defense // 2) + + defender.take_damage(damage) + + message_log.add( + f"Your ranged attack hits the {defender.name} for {damage} damage!", + COLOR_RANGED + ) + + # Check for death + if not defender.is_alive: + handle_death(target_entity, defender) + +# ============================================================================= +# Combat System +# ============================================================================= + +def calculate_damage(attacker: Fighter, defender: Fighter) -> int: + return max(0, attacker.attack - defender.defense) + +def perform_attack(attacker_entity: mcrfpy.Entity, defender_entity: mcrfpy.Entity) -> None: + global game_over + + attacker = entity_data.get(attacker_entity) + defender = entity_data.get(defender_entity) + + if attacker is None or defender is None: + return + + damage = calculate_damage(attacker, defender) + defender.take_damage(damage) + + if damage > 0: + if attacker.is_player: + message_log.add( + f"You hit the {defender.name} for {damage} damage!", + COLOR_PLAYER_ATTACK + ) + else: + message_log.add( + f"The {attacker.name} hits you for {damage} damage!", + COLOR_ENEMY_ATTACK + ) + else: + if attacker.is_player: + message_log.add( + f"You hit the {defender.name} but deal no damage.", + mcrfpy.Color(150, 150, 150) + ) + else: + message_log.add( + f"The {attacker.name} hits but deals no damage.", + mcrfpy.Color(150, 150, 200) + ) + + if not defender.is_alive: + handle_death(defender_entity, defender) + + update_ui() + +def handle_death(entity: mcrfpy.Entity, fighter: Fighter) -> None: + global game_over, grid + + if fighter.is_player: + message_log.add("You have died!", COLOR_PLAYER_DEATH) + message_log.add("Press R to restart or Escape to quit.", COLOR_INFO) + game_over = True + entity.sprite_index = SPRITE_CORPSE + else: + message_log.add(f"The {fighter.name} dies!", COLOR_ENEMY_DEATH) + remove_entity(grid, entity) + +# ============================================================================= +# Item Actions +# ============================================================================= + +def pickup_item() -> bool: + global player, player_inventory, grid + + px, py = int(player.grid_x), int(player.grid_y) + item_entity = get_item_at(grid, px, py) + + if item_entity is None: + message_log.add("There is nothing to pick up here.", COLOR_INVALID) + return False + + if player_inventory.is_full(): + message_log.add("Your inventory is full!", COLOR_WARNING) + return False + + item = item_data.get(item_entity) + if item is None: + return False + + player_inventory.add(item) + remove_item_entity(grid, item_entity) + + message_log.add(f"You pick up the {item.name}.", COLOR_PICKUP) + + update_ui() + return True + +def use_item(index: int) -> bool: + global player, player_inventory + + item = player_inventory.get(index) + if item is None: + message_log.add("Invalid item selection.", COLOR_INVALID) + return False + + if item.item_type == "health_potion": + fighter = entity_data.get(player) + if fighter is None: + return False + + if fighter.hp >= fighter.max_hp: + message_log.add("You are already at full health!", COLOR_WARNING) + return False + + actual_heal = fighter.heal(item.heal_amount) + player_inventory.remove(index) + + message_log.add(f"You drink the {item.name} and recover {actual_heal} HP!", COLOR_HEAL) + + update_ui() + return True + + message_log.add(f"You cannot use the {item.name}.", COLOR_INVALID) + return False + +# ============================================================================= +# Field of View +# ============================================================================= + +def update_entity_visibility(target_grid: mcrfpy.Grid) -> None: + global player, target_cursor + + for entity in target_grid.entities: + if entity == player: + entity.visible = True + continue + + # Targeting cursor is always visible during targeting mode + if entity == target_cursor: + entity.visible = True + continue + + ex, ey = int(entity.grid_x), int(entity.grid_y) + entity.visible = target_grid.is_in_fov(ex, ey) + +def update_fov(target_grid: mcrfpy.Grid, target_fov_layer, player_x: int, player_y: int) -> None: + target_grid.compute_fov((player_x, player_y), radius=FOV_RADIUS, algorithm=mcrfpy.FOV.SHADOW) + + for y in range(GRID_HEIGHT): + for x in range(GRID_WIDTH): + if target_grid.is_in_fov(x, y): + mark_explored(x, y) + target_fov_layer.set((x, y), COLOR_VISIBLE) + elif is_explored(x, y): + target_fov_layer.set((x, y), COLOR_DISCOVERED) + else: + target_fov_layer.set((x, y), COLOR_UNKNOWN) + + update_entity_visibility(target_grid) + +# ============================================================================= +# Movement and Actions +# ============================================================================= + +def can_move_to(target_grid: mcrfpy.Grid, x: int, y: int, mover: mcrfpy.Entity = None) -> bool: + if x < 0 or x >= GRID_WIDTH or y < 0 or y >= GRID_HEIGHT: + return False + + if not target_grid.at(x, y).walkable: + return False + + blocker = get_blocking_entity_at(target_grid, x, y, exclude=mover) + if blocker is not None: + return False + + return True + +def try_move_or_attack(dx: int, dy: int) -> None: + global player, grid, fov_layer, game_over + + if game_over: + return + + px, py = int(player.grid_x), int(player.grid_y) + target_x = px + dx + target_y = py + dy + + if target_x < 0 or target_x >= GRID_WIDTH or target_y < 0 or target_y >= GRID_HEIGHT: + return + + blocker = get_blocking_entity_at(grid, target_x, target_y, exclude=player) + + if blocker is not None: + perform_attack(player, blocker) + enemy_turn() + elif grid.at(target_x, target_y).walkable: + player.grid_x = target_x + player.grid_y = target_y + update_fov(grid, fov_layer, target_x, target_y) + enemy_turn() + + update_ui() + +# ============================================================================= +# Enemy AI +# ============================================================================= + +def enemy_turn() -> None: + global player, grid, game_over + + if game_over: + return + + player_x, player_y = int(player.grid_x), int(player.grid_y) + + enemies = [] + for entity in grid.entities: + if entity == player: + continue + if entity in entity_data and entity_data[entity].is_alive: + enemies.append(entity) + + for enemy in enemies: + fighter = entity_data.get(enemy) + if fighter is None or not fighter.is_alive: + continue + + ex, ey = int(enemy.grid_x), int(enemy.grid_y) + + if not grid.is_in_fov(ex, ey): + continue + + dx = player_x - ex + dy = player_y - ey + + if abs(dx) <= 1 and abs(dy) <= 1 and (dx != 0 or dy != 0): + perform_attack(enemy, player) + else: + move_toward_player(enemy, ex, ey, player_x, player_y) + +def move_toward_player(enemy: mcrfpy.Entity, ex: int, ey: int, px: int, py: int) -> None: + global grid + + dx = 0 + dy = 0 + + if px < ex: + dx = -1 + elif px > ex: + dx = 1 + + if py < ey: + dy = -1 + elif py > ey: + dy = 1 + + new_x = ex + dx + new_y = ey + dy + + if can_move_to(grid, new_x, new_y, enemy): + enemy.grid_x = new_x + enemy.grid_y = new_y + elif dx != 0 and can_move_to(grid, ex + dx, ey, enemy): + enemy.grid_x = ex + dx + elif dy != 0 and can_move_to(grid, ex, ey + dy, enemy): + enemy.grid_y = ey + dy + +# ============================================================================= +# UI Updates +# ============================================================================= + +def update_ui() -> None: + global player, health_bar, inventory_panel, player_inventory + + if player in entity_data: + fighter = entity_data[player] + health_bar.update(fighter.hp, fighter.max_hp) + + if player_inventory: + inventory_panel.update(player_inventory) + +# ============================================================================= +# Game Setup +# ============================================================================= + +# Create the scene +scene = mcrfpy.Scene("game") + +# Load texture +texture = mcrfpy.Texture("assets/kenney_tinydungeon.png", 16, 16) + +# Create the grid +grid = mcrfpy.Grid( + pos=(20, GAME_AREA_Y), + size=(700, GAME_AREA_HEIGHT - 20), + grid_size=(GRID_WIDTH, GRID_HEIGHT), + texture=texture, + zoom=1.0 +) + +# Generate initial dungeon +fill_with_walls(grid) +init_explored() + +rooms: list[RectangularRoom] = [] + +for _ in range(MAX_ROOMS): + room_width = random.randint(ROOM_MIN_SIZE, ROOM_MAX_SIZE) + room_height = random.randint(ROOM_MIN_SIZE, ROOM_MAX_SIZE) + x = random.randint(1, GRID_WIDTH - room_width - 2) + y = random.randint(1, GRID_HEIGHT - room_height - 2) + + new_room = RectangularRoom(x, y, room_width, room_height) + + overlaps = False + for other_room in rooms: + if new_room.intersects(other_room): + overlaps = True + break + + if overlaps: + continue + + carve_room(grid, new_room) + + if rooms: + carve_l_tunnel(grid, new_room.center, rooms[-1].center) + + rooms.append(new_room) + +# Get player starting position +if rooms: + player_start_x, player_start_y = rooms[0].center +else: + player_start_x, player_start_y = GRID_WIDTH // 2, GRID_HEIGHT // 2 + +# Add FOV layer +fov_layer = mcrfpy.ColorLayer(name="fov", z_index=-1) +grid.add_layer(fov_layer) +for y in range(GRID_HEIGHT): + for x in range(GRID_WIDTH): + fov_layer.set((x, y), COLOR_UNKNOWN) + +# Create the player +player = mcrfpy.Entity( + grid_pos=(player_start_x, player_start_y), + texture=texture, + sprite_index=SPRITE_PLAYER +) +grid.entities.append(player) + +entity_data[player] = Fighter( + hp=30, + max_hp=30, + attack=5, + defense=2, + name="Player", + is_player=True +) + +# Create player inventory +player_inventory = Inventory(capacity=10) + +# Spawn enemies and items +for i, room in enumerate(rooms): + if i == 0: + continue + spawn_enemies_in_room(grid, room, texture) + spawn_items_in_room(grid, room, texture) + +# Calculate initial FOV +update_fov(grid, fov_layer, player_start_x, player_start_y) + +# Add grid to scene +scene.children.append(grid) + +# ============================================================================= +# Create UI Elements +# ============================================================================= + +# Title bar +title = mcrfpy.Caption( + pos=(20, 10), + text="Part 9: Ranged Combat" +) +title.fill_color = mcrfpy.Color(255, 255, 255) +title.font_size = 24 +scene.children.append(title) + +# Instructions +instructions = mcrfpy.Caption( + pos=(280, 15), + text="WASD: Move | F: Ranged attack | G: Pickup | 1-5: Use | R: Restart" +) +instructions.fill_color = mcrfpy.Color(180, 180, 180) +instructions.font_size = 14 +scene.children.append(instructions) + +# Health Bar +health_bar = HealthBar( + x=730, + y=10, + width=280, + height=30 +) +health_bar.add_to_scene(scene) + +# Mode Display +mode_display = ModeDisplay(x=20, y=40) +mode_display.add_to_scene(scene) + +# Inventory Panel +inventory_panel = InventoryPanel( + x=730, + y=GAME_AREA_Y, + width=280, + height=150 +) +inventory_panel.add_to_scene(scene) + +# Message Log +message_log = MessageLog( + x=20, + y=768 - UI_BOTTOM_HEIGHT + 10, + width=990, + height=UI_BOTTOM_HEIGHT - 20, + max_messages=6 +) +message_log.add_to_scene(scene) + +# Initial messages +message_log.add("Welcome to the dungeon!", COLOR_INFO) +message_log.add("Press F to enter targeting mode for ranged attacks.", COLOR_INFO) + +# Initialize UI +update_ui() + +# ============================================================================= +# Input Handling +# ============================================================================= + +def restart_game() -> None: + global player, grid, fov_layer, game_over, entity_data, item_data, rooms + global player_inventory, game_mode, target_cursor + + # Exit targeting mode if active + if game_mode == GameMode.TARGETING: + exit_targeting_mode() + + game_over = False + game_mode = GameMode.NORMAL + + entity_data.clear() + item_data.clear() + + while len(grid.entities) > 0: + grid.entities.pop() + + fill_with_walls(grid) + init_explored() + message_log.clear() + + rooms = [] + + for _ in range(MAX_ROOMS): + room_width = random.randint(ROOM_MIN_SIZE, ROOM_MAX_SIZE) + room_height = random.randint(ROOM_MIN_SIZE, ROOM_MAX_SIZE) + x = random.randint(1, GRID_WIDTH - room_width - 2) + y = random.randint(1, GRID_HEIGHT - room_height - 2) + + new_room = RectangularRoom(x, y, room_width, room_height) + + overlaps = False + for other_room in rooms: + if new_room.intersects(other_room): + overlaps = True + break + + if overlaps: + continue + + carve_room(grid, new_room) + + if rooms: + carve_l_tunnel(grid, new_room.center, rooms[-1].center) + + rooms.append(new_room) + + if rooms: + new_x, new_y = rooms[0].center + else: + new_x, new_y = GRID_WIDTH // 2, GRID_HEIGHT // 2 + + player = mcrfpy.Entity( + grid_pos=(new_x, new_y), + texture=texture, + sprite_index=SPRITE_PLAYER + ) + grid.entities.append(player) + + entity_data[player] = Fighter( + hp=30, + max_hp=30, + attack=5, + defense=2, + name="Player", + is_player=True + ) + + player_inventory = Inventory(capacity=10) + + for i, room in enumerate(rooms): + if i == 0: + continue + spawn_enemies_in_room(grid, room, texture) + spawn_items_in_room(grid, room, texture) + + for y in range(GRID_HEIGHT): + for x in range(GRID_WIDTH): + fov_layer.set((x, y), COLOR_UNKNOWN) + + update_fov(grid, fov_layer, new_x, new_y) + + message_log.add("A new adventure begins!", COLOR_INFO) + + mode_display.update(game_mode) + update_ui() + +NUMBER_KEYS = [ + mcrfpy.Key.NUM_1, + mcrfpy.Key.NUM_2, + mcrfpy.Key.NUM_3, + mcrfpy.Key.NUM_4, + mcrfpy.Key.NUM_5, +] + +def handle_keys(key: mcrfpy.Key, action: mcrfpy.InputState) -> None: + global game_over, game_mode + + if action != mcrfpy.InputState.PRESSED: + return + + # Always allow restart and quit + if key == mcrfpy.Key.R: + restart_game() + return + + if key == mcrfpy.Key.ESCAPE: + if game_mode == GameMode.TARGETING: + exit_targeting_mode() + message_log.add("Targeting cancelled.", COLOR_INFO) + return + else: + mcrfpy.exit() + return + + if game_over: + return + + # Handle input based on game mode + if game_mode == GameMode.TARGETING: + handle_targeting_input(key) + else: + handle_normal_input(key) + +def handle_normal_input(key: mcrfpy.Key) -> None: + """Handle input in normal game mode.""" + # Movement + if key == mcrfpy.Key.W or key == mcrfpy.Key.UP: + try_move_or_attack(0, -1) + elif key == mcrfpy.Key.S or key == mcrfpy.Key.DOWN: + try_move_or_attack(0, 1) + elif key == mcrfpy.Key.A or key == mcrfpy.Key.LEFT: + try_move_or_attack(-1, 0) + elif key == mcrfpy.Key.D or key == mcrfpy.Key.RIGHT: + try_move_or_attack(1, 0) + # Ranged attack (enter targeting mode) + elif key == mcrfpy.Key.F: + enter_targeting_mode() + # Pickup + elif key == mcrfpy.Key.G or key == mcrfpy.Key.COMMA: + pickup_item() + # Use items + elif key in NUMBER_KEYS: + index = NUMBER_KEYS.index(key) + if use_item(index): + enemy_turn() + update_ui() + +def handle_targeting_input(key: mcrfpy.Key) -> None: + """Handle input in targeting mode.""" + if key == mcrfpy.Key.UP or key == mcrfpy.Key.W: + move_cursor(0, -1) + elif key == mcrfpy.Key.DOWN or key == mcrfpy.Key.S: + move_cursor(0, 1) + elif key == mcrfpy.Key.LEFT or key == mcrfpy.Key.A: + move_cursor(-1, 0) + elif key == mcrfpy.Key.RIGHT or key == mcrfpy.Key.D: + move_cursor(1, 0) + elif key == mcrfpy.Key.ENTER or key == mcrfpy.Key.SPACE: + confirm_target() + +scene.on_key = handle_keys + +# ============================================================================= +# Start the Game +# ============================================================================= + +scene.activate() +print("Part 9 loaded! Press F to enter targeting mode for ranged attacks.") diff --git a/tests/snippets/280_part_10_save_load.py b/tests/snippets/280_part_10_save_load.py new file mode 100644 index 0000000..3565b09 --- /dev/null +++ b/tests/snippets/280_part_10_save_load.py @@ -0,0 +1,1561 @@ +# mcrf: objects=[Caption,Color,ColorLayer,Entity,FOV,Frame,Grid,InputState,Key,Scene,Texture] verified=0.2.8-dev status=ok +"""McRogueFace Tutorial - Part 10: Saving and Loading + +Implement save/load functionality with JSON serialization. +""" +import mcrfpy +import random +import json +import os +from dataclasses import dataclass, field +from typing import Optional +from enum import Enum + +# ============================================================================= +# Constants +# ============================================================================= + +# Sprite indices for CP437 tileset +SPRITE_WALL = 35 # '#' - wall +SPRITE_FLOOR = 46 # '.' - floor +SPRITE_PLAYER = 64 # '@' - player +SPRITE_CORPSE = 37 # '%' - remains +SPRITE_POTION = 173 # Potion sprite +SPRITE_CURSOR = 88 # 'X' - targeting cursor + +# Enemy sprites +SPRITE_GOBLIN = 103 # 'g' +SPRITE_ORC = 111 # 'o' +SPRITE_TROLL = 116 # 't' + +# Grid dimensions +GRID_WIDTH = 50 +GRID_HEIGHT = 30 + +# Room generation parameters +ROOM_MIN_SIZE = 6 +ROOM_MAX_SIZE = 12 +MAX_ROOMS = 8 + +# Enemy spawn parameters +MAX_ENEMIES_PER_ROOM = 3 + +# Item spawn parameters +MAX_ITEMS_PER_ROOM = 2 + +# FOV and targeting settings +FOV_RADIUS = 8 +RANGED_ATTACK_RANGE = 6 +RANGED_ATTACK_DAMAGE = 4 + +# Save file location +SAVE_FILE = "savegame.json" + +# Visibility colors +COLOR_VISIBLE = mcrfpy.Color(0, 0, 0, 0) +COLOR_DISCOVERED = mcrfpy.Color(0, 0, 40, 180) +COLOR_UNKNOWN = mcrfpy.Color(0, 0, 0, 255) + +# Message colors +COLOR_PLAYER_ATTACK = mcrfpy.Color(200, 200, 200) +COLOR_ENEMY_ATTACK = mcrfpy.Color(255, 150, 150) +COLOR_PLAYER_DEATH = mcrfpy.Color(255, 50, 50) +COLOR_ENEMY_DEATH = mcrfpy.Color(100, 255, 100) +COLOR_HEAL = mcrfpy.Color(100, 255, 100) +COLOR_PICKUP = mcrfpy.Color(100, 200, 255) +COLOR_INFO = mcrfpy.Color(100, 100, 255) +COLOR_WARNING = mcrfpy.Color(255, 200, 50) +COLOR_INVALID = mcrfpy.Color(255, 100, 100) +COLOR_RANGED = mcrfpy.Color(255, 255, 100) +COLOR_SAVE = mcrfpy.Color(100, 255, 200) + +# UI Layout constants +UI_TOP_HEIGHT = 60 +UI_BOTTOM_HEIGHT = 150 +GAME_AREA_Y = UI_TOP_HEIGHT +GAME_AREA_HEIGHT = 768 - UI_TOP_HEIGHT - UI_BOTTOM_HEIGHT + +# ============================================================================= +# Game Modes +# ============================================================================= + +class GameMode(Enum): + NORMAL = "normal" + TARGETING = "targeting" + +# ============================================================================= +# Fighter Component +# ============================================================================= + +@dataclass +class Fighter: + """Combat stats for an entity.""" + hp: int + max_hp: int + attack: int + defense: int + name: str + is_player: bool = False + + @property + def is_alive(self) -> bool: + return self.hp > 0 + + def take_damage(self, amount: int) -> int: + actual_damage = min(self.hp, amount) + self.hp -= actual_damage + return actual_damage + + def heal(self, amount: int) -> int: + actual_heal = min(self.max_hp - self.hp, amount) + self.hp += actual_heal + return actual_heal + + def to_dict(self) -> dict: + """Serialize fighter data to dictionary.""" + return { + "hp": self.hp, + "max_hp": self.max_hp, + "attack": self.attack, + "defense": self.defense, + "name": self.name, + "is_player": self.is_player + } + + @classmethod + def from_dict(cls, data: dict) -> "Fighter": + """Deserialize fighter data from dictionary.""" + return cls( + hp=data["hp"], + max_hp=data["max_hp"], + attack=data["attack"], + defense=data["defense"], + name=data["name"], + is_player=data.get("is_player", False) + ) + +# ============================================================================= +# Item Component +# ============================================================================= + +@dataclass +class Item: + """Data for an item that can be picked up and used.""" + name: str + item_type: str + heal_amount: int = 0 + + def to_dict(self) -> dict: + """Serialize item data to dictionary.""" + return { + "name": self.name, + "item_type": self.item_type, + "heal_amount": self.heal_amount + } + + @classmethod + def from_dict(cls, data: dict) -> "Item": + """Deserialize item data from dictionary.""" + return cls( + name=data["name"], + item_type=data["item_type"], + heal_amount=data.get("heal_amount", 0) + ) + +# ============================================================================= +# Inventory System +# ============================================================================= + +@dataclass +class Inventory: + """Container for items the player is carrying.""" + capacity: int = 10 + items: list = field(default_factory=list) + + def add(self, item: Item) -> bool: + if len(self.items) >= self.capacity: + return False + self.items.append(item) + return True + + def remove(self, index: int) -> Optional[Item]: + if 0 <= index < len(self.items): + return self.items.pop(index) + return None + + def get(self, index: int) -> Optional[Item]: + if 0 <= index < len(self.items): + return self.items[index] + return None + + def is_full(self) -> bool: + return len(self.items) >= self.capacity + + def count(self) -> int: + return len(self.items) + + def to_dict(self) -> dict: + """Serialize inventory to dictionary.""" + return { + "capacity": self.capacity, + "items": [item.to_dict() for item in self.items] + } + + @classmethod + def from_dict(cls, data: dict) -> "Inventory": + """Deserialize inventory from dictionary.""" + inv = cls(capacity=data.get("capacity", 10)) + inv.items = [Item.from_dict(item_data) for item_data in data.get("items", [])] + return inv + +# ============================================================================= +# Templates +# ============================================================================= + +ITEM_TEMPLATES = { + "health_potion": { + "name": "Health Potion", + "sprite": SPRITE_POTION, + "item_type": "health_potion", + "heal_amount": 10 + } +} + +ENEMY_TEMPLATES = { + "goblin": { + "sprite": SPRITE_GOBLIN, + "hp": 6, + "attack": 3, + "defense": 0 + }, + "orc": { + "sprite": SPRITE_ORC, + "hp": 10, + "attack": 4, + "defense": 1 + }, + "troll": { + "sprite": SPRITE_TROLL, + "hp": 16, + "attack": 6, + "defense": 2 + } +} + +# ============================================================================= +# Message Log System +# ============================================================================= + +class MessageLog: + """A message log that displays recent game messages with colors.""" + + def __init__(self, x: int, y: int, width: int, height: int, max_messages: int = 6): + self.x = x + self.y = y + self.width = width + self.height = height + self.max_messages = max_messages + self.messages: list[tuple[str, mcrfpy.Color]] = [] + self.captions: list[mcrfpy.Caption] = [] + + self.frame = mcrfpy.Frame( + pos=(x, y), + size=(width, height) + ) + self.frame.fill_color = mcrfpy.Color(20, 20, 30, 200) + self.frame.outline = 2 + self.frame.outline_color = mcrfpy.Color(80, 80, 100) + + line_height = 20 + for i in range(max_messages): + caption = mcrfpy.Caption( + pos=(x + 10, y + 5 + i * line_height), + text="" + ) + caption.font_size = 14 + caption.fill_color = mcrfpy.Color(200, 200, 200) + self.captions.append(caption) + + def add_to_scene(self, scene: mcrfpy.Scene) -> None: + scene.children.append(self.frame) + for caption in self.captions: + scene.children.append(caption) + + def add(self, text: str, color: mcrfpy.Color = None) -> None: + if color is None: + color = mcrfpy.Color(200, 200, 200) + + self.messages.append((text, color)) + + while len(self.messages) > self.max_messages: + self.messages.pop(0) + + self._refresh() + + def _refresh(self) -> None: + for i, caption in enumerate(self.captions): + if i < len(self.messages): + text, color = self.messages[i] + caption.text = text + caption.fill_color = color + else: + caption.text = "" + + def clear(self) -> None: + self.messages.clear() + self._refresh() + +# ============================================================================= +# Health Bar System +# ============================================================================= + +class HealthBar: + """A visual health bar using nested frames.""" + + def __init__(self, x: int, y: int, width: int, height: int): + self.x = x + self.y = y + self.width = width + self.height = height + self.max_hp = 30 + self.current_hp = 30 + + self.bg_frame = mcrfpy.Frame( + pos=(x, y), + size=(width, height) + ) + self.bg_frame.fill_color = mcrfpy.Color(80, 0, 0) + self.bg_frame.outline = 2 + self.bg_frame.outline_color = mcrfpy.Color(150, 150, 150) + + self.fg_frame = mcrfpy.Frame( + pos=(x + 2, y + 2), + size=(width - 4, height - 4) + ) + self.fg_frame.fill_color = mcrfpy.Color(0, 180, 0) + self.fg_frame.outline = 0 + + self.label = mcrfpy.Caption( + pos=(x + 5, y + 2), + text=f"HP: {self.current_hp}/{self.max_hp}" + ) + self.label.font_size = 16 + self.label.fill_color = mcrfpy.Color(255, 255, 255) + + def add_to_scene(self, scene: mcrfpy.Scene) -> None: + scene.children.append(self.bg_frame) + scene.children.append(self.fg_frame) + scene.children.append(self.label) + + def update(self, current_hp: int, max_hp: int) -> None: + self.current_hp = current_hp + self.max_hp = max_hp + + percent = max(0, current_hp / max_hp) if max_hp > 0 else 0 + + inner_width = self.width - 4 + self.fg_frame.resize(int(inner_width * percent), self.height - 4) + + self.label.text = f"HP: {current_hp}/{max_hp}" + + if percent > 0.6: + self.fg_frame.fill_color = mcrfpy.Color(0, 180, 0) + elif percent > 0.3: + self.fg_frame.fill_color = mcrfpy.Color(180, 180, 0) + else: + self.fg_frame.fill_color = mcrfpy.Color(180, 0, 0) + +# ============================================================================= +# Inventory Panel +# ============================================================================= + +class InventoryPanel: + """A panel displaying the player's inventory.""" + + def __init__(self, x: int, y: int, width: int, height: int): + self.x = x + self.y = y + self.width = width + self.height = height + self.captions: list[mcrfpy.Caption] = [] + + self.frame = mcrfpy.Frame( + pos=(x, y), + size=(width, height) + ) + self.frame.fill_color = mcrfpy.Color(20, 20, 30, 200) + self.frame.outline = 2 + self.frame.outline_color = mcrfpy.Color(80, 80, 100) + + self.title = mcrfpy.Caption( + pos=(x + 10, y + 5), + text="Inventory (G:pickup, 1-5:use)" + ) + self.title.font_size = 14 + self.title.fill_color = mcrfpy.Color(200, 200, 255) + + for i in range(5): + caption = mcrfpy.Caption( + pos=(x + 10, y + 25 + i * 18), + text="" + ) + caption.font_size = 13 + caption.fill_color = mcrfpy.Color(180, 180, 180) + self.captions.append(caption) + + def add_to_scene(self, scene: mcrfpy.Scene) -> None: + scene.children.append(self.frame) + scene.children.append(self.title) + for caption in self.captions: + scene.children.append(caption) + + def update(self, inventory: Inventory) -> None: + for i, caption in enumerate(self.captions): + if i < len(inventory.items): + item = inventory.items[i] + caption.text = f"{i+1}. {item.name}" + caption.fill_color = mcrfpy.Color(180, 180, 180) + else: + caption.text = f"{i+1}. ---" + caption.fill_color = mcrfpy.Color(80, 80, 80) + +# ============================================================================= +# Mode Display +# ============================================================================= + +class ModeDisplay: + """Displays the current game mode.""" + + def __init__(self, x: int, y: int): + self.caption = mcrfpy.Caption( + pos=(x, y), + text="[NORMAL MODE]" + ) + self.caption.font_size = 16 + self.caption.fill_color = mcrfpy.Color(100, 255, 100) + + def add_to_scene(self, scene: mcrfpy.Scene) -> None: + scene.children.append(self.caption) + + def update(self, mode: GameMode) -> None: + if mode == GameMode.NORMAL: + self.caption.text = "[NORMAL] F:Ranged | S:Save" + self.caption.fill_color = mcrfpy.Color(100, 255, 100) + elif mode == GameMode.TARGETING: + self.caption.text = "[TARGETING] Arrows:Move, Enter:Fire, Esc:Cancel" + self.caption.fill_color = mcrfpy.Color(255, 255, 100) + +# ============================================================================= +# Global State +# ============================================================================= + +entity_data: dict[mcrfpy.Entity, Fighter] = {} +item_data: dict[mcrfpy.Entity, Item] = {} + +player: Optional[mcrfpy.Entity] = None +player_inventory: Optional[Inventory] = None +grid: Optional[mcrfpy.Grid] = None +fov_layer = None +texture: Optional[mcrfpy.Texture] = None +game_over: bool = False +dungeon_level: int = 1 + +# Game mode state +game_mode: GameMode = GameMode.NORMAL +target_cursor: Optional[mcrfpy.Entity] = None +target_x: int = 0 +target_y: int = 0 + +# UI components +message_log: Optional[MessageLog] = None +health_bar: Optional[HealthBar] = None +inventory_panel: Optional[InventoryPanel] = None +mode_display: Optional[ModeDisplay] = None + +# ============================================================================= +# Room Class +# ============================================================================= + +class RectangularRoom: + """A rectangular room with its position and size.""" + + def __init__(self, x: int, y: int, width: int, height: int): + self.x1 = x + self.y1 = y + self.x2 = x + width + self.y2 = y + height + + @property + def center(self) -> tuple[int, int]: + center_x = (self.x1 + self.x2) // 2 + center_y = (self.y1 + self.y2) // 2 + return center_x, center_y + + @property + def inner(self) -> tuple[slice, slice]: + return slice(self.x1 + 1, self.x2), slice(self.y1 + 1, self.y2) + + def intersects(self, other: "RectangularRoom") -> bool: + return ( + self.x1 <= other.x2 and + self.x2 >= other.x1 and + self.y1 <= other.y2 and + self.y2 >= other.y1 + ) + +# ============================================================================= +# Exploration Tracking +# ============================================================================= + +explored: list[list[bool]] = [] + +def init_explored() -> None: + global explored + explored = [[False for _ in range(GRID_WIDTH)] for _ in range(GRID_HEIGHT)] + +def mark_explored(x: int, y: int) -> None: + if 0 <= x < GRID_WIDTH and 0 <= y < GRID_HEIGHT: + explored[y][x] = True + +def is_explored(x: int, y: int) -> bool: + if 0 <= x < GRID_WIDTH and 0 <= y < GRID_HEIGHT: + return explored[y][x] + return False + +# ============================================================================= +# Save/Load System +# ============================================================================= + +def save_game() -> bool: + """Save the current game state to a JSON file. + + Returns: + True if save succeeded, False otherwise + """ + global player, player_inventory, grid, explored, dungeon_level + + try: + # Collect tile data + tiles = [] + for y in range(GRID_HEIGHT): + row = [] + for x in range(GRID_WIDTH): + cell = grid.at(x, y) + row.append({ + "tilesprite": cell.tilesprite, + "walkable": cell.walkable, + "transparent": cell.transparent + }) + tiles.append(row) + + # Collect enemy data + enemies = [] + for entity in grid.entities: + if entity == player: + continue + if entity in entity_data: + fighter = entity_data[entity] + enemies.append({ + "x": int(entity.grid_x), + "y": int(entity.grid_y), + "type": fighter.name.lower(), + "fighter": fighter.to_dict() + }) + + # Collect ground item data + items_on_ground = [] + for entity in grid.entities: + if entity in item_data: + item = item_data[entity] + items_on_ground.append({ + "x": int(entity.grid_x), + "y": int(entity.grid_y), + "item": item.to_dict() + }) + + # Build save data structure + save_data = { + "version": 1, # For future compatibility + "dungeon_level": dungeon_level, + "player": { + "x": int(player.grid_x), + "y": int(player.grid_y), + "fighter": entity_data[player].to_dict(), + "inventory": player_inventory.to_dict() + }, + "tiles": tiles, + "explored": [[explored[y][x] for x in range(GRID_WIDTH)] for y in range(GRID_HEIGHT)], + "enemies": enemies, + "items": items_on_ground + } + + # Write to file + with open(SAVE_FILE, "w") as f: + json.dump(save_data, f, indent=2) + + message_log.add("Game saved successfully!", COLOR_SAVE) + return True + + except Exception as e: + message_log.add(f"Failed to save: {str(e)}", COLOR_INVALID) + print(f"Save error: {e}") + return False + +def load_game() -> bool: + """Load a saved game from JSON file. + + Returns: + True if load succeeded, False otherwise + """ + global player, player_inventory, grid, explored, dungeon_level + global entity_data, item_data, fov_layer, game_over + + if not os.path.exists(SAVE_FILE): + return False + + try: + with open(SAVE_FILE, "r") as f: + save_data = json.load(f) + + # Clear current game state + entity_data.clear() + item_data.clear() + + while len(grid.entities) > 0: + grid.entities.pop() + + # Restore dungeon level + dungeon_level = save_data.get("dungeon_level", 1) + + # Restore tiles + tiles = save_data["tiles"] + for y in range(GRID_HEIGHT): + for x in range(GRID_WIDTH): + cell = grid.at(x, y) + tile_data = tiles[y][x] + cell.tilesprite = tile_data["tilesprite"] + cell.walkable = tile_data["walkable"] + cell.transparent = tile_data["transparent"] + + # Restore explored state + global explored + explored_data = save_data["explored"] + explored = [[explored_data[y][x] for x in range(GRID_WIDTH)] for y in range(GRID_HEIGHT)] + + # Restore player + player_data = save_data["player"] + player = mcrfpy.Entity( + grid_pos=(player_data["x"], player_data["y"]), + texture=texture, + sprite_index=SPRITE_PLAYER + ) + grid.entities.append(player) + + entity_data[player] = Fighter.from_dict(player_data["fighter"]) + player_inventory = Inventory.from_dict(player_data["inventory"]) + + # Restore enemies + for enemy_data in save_data.get("enemies", []): + enemy_type = enemy_data["type"] + template = ENEMY_TEMPLATES.get(enemy_type, ENEMY_TEMPLATES["goblin"]) + + enemy = mcrfpy.Entity( + grid_pos=(enemy_data["x"], enemy_data["y"]), + texture=texture, + sprite_index=template["sprite"] + ) + enemy.visible = False + + grid.entities.append(enemy) + entity_data[enemy] = Fighter.from_dict(enemy_data["fighter"]) + + # Restore ground items + for item_entry in save_data.get("items", []): + template = ITEM_TEMPLATES.get( + item_entry["item"]["item_type"], + ITEM_TEMPLATES["health_potion"] + ) + + item_entity = mcrfpy.Entity( + grid_pos=(item_entry["x"], item_entry["y"]), + texture=texture, + sprite_index=template["sprite"] + ) + item_entity.visible = False + + grid.entities.append(item_entity) + item_data[item_entity] = Item.from_dict(item_entry["item"]) + + # Reset FOV layer + for y in range(GRID_HEIGHT): + for x in range(GRID_WIDTH): + fov_layer.set((x, y), COLOR_UNKNOWN) + + # Compute initial FOV + update_fov(grid, fov_layer, int(player.grid_x), int(player.grid_y)) + + game_over = False + + message_log.add("Game loaded successfully!", COLOR_SAVE) + update_ui() + return True + + except Exception as e: + message_log.add(f"Failed to load: {str(e)}", COLOR_INVALID) + print(f"Load error: {e}") + return False + +def delete_save() -> bool: + """Delete the save file. + + Returns: + True if deletion succeeded or file did not exist + """ + try: + if os.path.exists(SAVE_FILE): + os.remove(SAVE_FILE) + return True + except Exception as e: + print(f"Delete save error: {e}") + return False + +def has_save_file() -> bool: + """Check if a save file exists.""" + return os.path.exists(SAVE_FILE) + +# ============================================================================= +# Dungeon Generation +# ============================================================================= + +def fill_with_walls(target_grid: mcrfpy.Grid) -> None: + for y in range(GRID_HEIGHT): + for x in range(GRID_WIDTH): + cell = target_grid.at(x, y) + cell.tilesprite = SPRITE_WALL + cell.walkable = False + cell.transparent = False + +def carve_room(target_grid: mcrfpy.Grid, room: RectangularRoom) -> None: + inner_x, inner_y = room.inner + for y in range(inner_y.start, inner_y.stop): + for x in range(inner_x.start, inner_x.stop): + if 0 <= x < GRID_WIDTH and 0 <= y < GRID_HEIGHT: + cell = target_grid.at(x, y) + cell.tilesprite = SPRITE_FLOOR + cell.walkable = True + cell.transparent = True + +def carve_tunnel_horizontal(target_grid: mcrfpy.Grid, x1: int, x2: int, y: int) -> None: + for x in range(min(x1, x2), max(x1, x2) + 1): + if 0 <= x < GRID_WIDTH and 0 <= y < GRID_HEIGHT: + cell = target_grid.at(x, y) + cell.tilesprite = SPRITE_FLOOR + cell.walkable = True + cell.transparent = True + +def carve_tunnel_vertical(target_grid: mcrfpy.Grid, y1: int, y2: int, x: int) -> None: + for y in range(min(y1, y2), max(y1, y2) + 1): + if 0 <= x < GRID_WIDTH and 0 <= y < GRID_HEIGHT: + cell = target_grid.at(x, y) + cell.tilesprite = SPRITE_FLOOR + cell.walkable = True + cell.transparent = True + +def carve_l_tunnel( + target_grid: mcrfpy.Grid, + start: tuple[int, int], + end: tuple[int, int] +) -> None: + x1, y1 = start + x2, y2 = end + + if random.random() < 0.5: + carve_tunnel_horizontal(target_grid, x1, x2, y1) + carve_tunnel_vertical(target_grid, y1, y2, x2) + else: + carve_tunnel_vertical(target_grid, y1, y2, x1) + carve_tunnel_horizontal(target_grid, x1, x2, y2) + +# ============================================================================= +# Entity Management +# ============================================================================= + +def spawn_enemy(target_grid: mcrfpy.Grid, x: int, y: int, enemy_type: str, tex: mcrfpy.Texture) -> mcrfpy.Entity: + template = ENEMY_TEMPLATES[enemy_type] + + enemy = mcrfpy.Entity( + grid_pos=(x, y), + texture=tex, + sprite_index=template["sprite"] + ) + enemy.visible = False + + target_grid.entities.append(enemy) + + entity_data[enemy] = Fighter( + hp=template["hp"], + max_hp=template["hp"], + attack=template["attack"], + defense=template["defense"], + name=enemy_type.capitalize(), + is_player=False + ) + + return enemy + +def spawn_enemies_in_room(target_grid: mcrfpy.Grid, room: RectangularRoom, tex: mcrfpy.Texture) -> None: + num_enemies = random.randint(0, MAX_ENEMIES_PER_ROOM) + + for _ in range(num_enemies): + inner_x, inner_y = room.inner + x = random.randint(inner_x.start, inner_x.stop - 1) + y = random.randint(inner_y.start, inner_y.stop - 1) + + if is_position_occupied(target_grid, x, y): + continue + + roll = random.random() + if roll < 0.6: + enemy_type = "goblin" + elif roll < 0.9: + enemy_type = "orc" + else: + enemy_type = "troll" + + spawn_enemy(target_grid, x, y, enemy_type, tex) + +def spawn_item(target_grid: mcrfpy.Grid, x: int, y: int, item_type: str, tex: mcrfpy.Texture) -> mcrfpy.Entity: + template = ITEM_TEMPLATES[item_type] + + item_entity = mcrfpy.Entity( + grid_pos=(x, y), + texture=tex, + sprite_index=template["sprite"] + ) + item_entity.visible = False + + target_grid.entities.append(item_entity) + + item_data[item_entity] = Item( + name=template["name"], + item_type=template["item_type"], + heal_amount=template.get("heal_amount", 0) + ) + + return item_entity + +def spawn_items_in_room(target_grid: mcrfpy.Grid, room: RectangularRoom, tex: mcrfpy.Texture) -> None: + num_items = random.randint(0, MAX_ITEMS_PER_ROOM) + + for _ in range(num_items): + inner_x, inner_y = room.inner + x = random.randint(inner_x.start, inner_x.stop - 1) + y = random.randint(inner_y.start, inner_y.stop - 1) + + if is_position_occupied(target_grid, x, y): + continue + + spawn_item(target_grid, x, y, "health_potion", tex) + +def is_position_occupied(target_grid: mcrfpy.Grid, x: int, y: int) -> bool: + for entity in target_grid.entities: + if int(entity.grid_x) == x and int(entity.grid_y) == y: + return True + return False + +def get_item_at(target_grid: mcrfpy.Grid, x: int, y: int) -> Optional[mcrfpy.Entity]: + for entity in target_grid.entities: + if entity in item_data: + if int(entity.grid_x) == x and int(entity.grid_y) == y: + return entity + return None + +def get_blocking_entity_at(target_grid: mcrfpy.Grid, x: int, y: int, exclude: mcrfpy.Entity = None) -> Optional[mcrfpy.Entity]: + for entity in target_grid.entities: + if entity == exclude: + continue + if int(entity.grid_x) == x and int(entity.grid_y) == y: + if entity in entity_data and entity_data[entity].is_alive: + return entity + return None + +def remove_entity(target_grid: mcrfpy.Grid, entity: mcrfpy.Entity) -> None: + for e in target_grid.entities: + if e == entity: + target_grid.entities.remove(e) + break + if entity in entity_data: + del entity_data[entity] + +def remove_item_entity(target_grid: mcrfpy.Grid, entity: mcrfpy.Entity) -> None: + for e in target_grid.entities: + if e == entity: + target_grid.entities.remove(e) + break + if entity in item_data: + del item_data[entity] + +# ============================================================================= +# Targeting System +# ============================================================================= + +def enter_targeting_mode() -> None: + global game_mode, target_cursor, target_x, target_y, player, grid, texture + + target_x = int(player.grid_x) + target_y = int(player.grid_y) + + target_cursor = mcrfpy.Entity( + grid_pos=(target_x, target_y), + texture=texture, + sprite_index=SPRITE_CURSOR + ) + grid.entities.append(target_cursor) + + game_mode = GameMode.TARGETING + + message_log.add("Targeting mode: Arrows to aim, Enter to fire, Esc to cancel.", COLOR_INFO) + mode_display.update(game_mode) + +def exit_targeting_mode() -> None: + global game_mode, target_cursor, grid + + if target_cursor is not None: + for e in grid.entities: + if e == target_cursor: + grid.entities.remove(e) + break + target_cursor = None + + game_mode = GameMode.NORMAL + mode_display.update(game_mode) + +def move_cursor(dx: int, dy: int) -> None: + global target_x, target_y, target_cursor, grid, player + + new_x = target_x + dx + new_y = target_y + dy + + if new_x < 0 or new_x >= GRID_WIDTH or new_y < 0 or new_y >= GRID_HEIGHT: + return + + if not grid.is_in_fov(new_x, new_y): + message_log.add("You cannot see that location.", COLOR_INVALID) + return + + player_x, player_y = int(player.grid_x), int(player.grid_y) + distance = abs(new_x - player_x) + abs(new_y - player_y) + if distance > RANGED_ATTACK_RANGE: + message_log.add(f"Target is out of range (max {RANGED_ATTACK_RANGE}).", COLOR_WARNING) + return + + target_x = new_x + target_y = new_y + target_cursor.grid_pos = (target_x, target_y) + + enemy = get_blocking_entity_at(grid, target_x, target_y, exclude=player) + if enemy and enemy in entity_data: + fighter = entity_data[enemy] + message_log.add(f"Target: {fighter.name} (HP: {fighter.hp}/{fighter.max_hp})", COLOR_RANGED) + +def confirm_target() -> None: + global game_mode, target_x, target_y, player, grid + + if target_x == int(player.grid_x) and target_y == int(player.grid_y): + message_log.add("You cannot target yourself!", COLOR_INVALID) + return + + target_enemy = get_blocking_entity_at(grid, target_x, target_y, exclude=player) + + if target_enemy is None or target_enemy not in entity_data: + message_log.add("No valid target at that location.", COLOR_INVALID) + return + + perform_ranged_attack(target_enemy) + exit_targeting_mode() + enemy_turn() + update_ui() + +def perform_ranged_attack(target_entity: mcrfpy.Entity) -> None: + global player, game_over + + defender = entity_data.get(target_entity) + attacker = entity_data.get(player) + + if defender is None or attacker is None: + return + + damage = max(1, RANGED_ATTACK_DAMAGE - defender.defense // 2) + + defender.take_damage(damage) + + message_log.add( + f"Your ranged attack hits the {defender.name} for {damage} damage!", + COLOR_RANGED + ) + + if not defender.is_alive: + handle_death(target_entity, defender) + +# ============================================================================= +# Combat System +# ============================================================================= + +def calculate_damage(attacker: Fighter, defender: Fighter) -> int: + return max(0, attacker.attack - defender.defense) + +def perform_attack(attacker_entity: mcrfpy.Entity, defender_entity: mcrfpy.Entity) -> None: + global game_over + + attacker = entity_data.get(attacker_entity) + defender = entity_data.get(defender_entity) + + if attacker is None or defender is None: + return + + damage = calculate_damage(attacker, defender) + defender.take_damage(damage) + + if damage > 0: + if attacker.is_player: + message_log.add( + f"You hit the {defender.name} for {damage} damage!", + COLOR_PLAYER_ATTACK + ) + else: + message_log.add( + f"The {attacker.name} hits you for {damage} damage!", + COLOR_ENEMY_ATTACK + ) + else: + if attacker.is_player: + message_log.add( + f"You hit the {defender.name} but deal no damage.", + mcrfpy.Color(150, 150, 150) + ) + else: + message_log.add( + f"The {attacker.name} hits but deals no damage.", + mcrfpy.Color(150, 150, 200) + ) + + if not defender.is_alive: + handle_death(defender_entity, defender) + + update_ui() + +def handle_death(entity: mcrfpy.Entity, fighter: Fighter) -> None: + global game_over, grid + + if fighter.is_player: + message_log.add("You have died!", COLOR_PLAYER_DEATH) + message_log.add("Press R to restart or Escape to quit.", COLOR_INFO) + game_over = True + entity.sprite_index = SPRITE_CORPSE + # Delete save on death (permadeath!) + delete_save() + else: + message_log.add(f"The {fighter.name} dies!", COLOR_ENEMY_DEATH) + remove_entity(grid, entity) + +# ============================================================================= +# Item Actions +# ============================================================================= + +def pickup_item() -> bool: + global player, player_inventory, grid + + px, py = int(player.grid_x), int(player.grid_y) + item_entity = get_item_at(grid, px, py) + + if item_entity is None: + message_log.add("There is nothing to pick up here.", COLOR_INVALID) + return False + + if player_inventory.is_full(): + message_log.add("Your inventory is full!", COLOR_WARNING) + return False + + item = item_data.get(item_entity) + if item is None: + return False + + player_inventory.add(item) + remove_item_entity(grid, item_entity) + + message_log.add(f"You pick up the {item.name}.", COLOR_PICKUP) + + update_ui() + return True + +def use_item(index: int) -> bool: + global player, player_inventory + + item = player_inventory.get(index) + if item is None: + message_log.add("Invalid item selection.", COLOR_INVALID) + return False + + if item.item_type == "health_potion": + fighter = entity_data.get(player) + if fighter is None: + return False + + if fighter.hp >= fighter.max_hp: + message_log.add("You are already at full health!", COLOR_WARNING) + return False + + actual_heal = fighter.heal(item.heal_amount) + player_inventory.remove(index) + + message_log.add(f"You drink the {item.name} and recover {actual_heal} HP!", COLOR_HEAL) + + update_ui() + return True + + message_log.add(f"You cannot use the {item.name}.", COLOR_INVALID) + return False + +# ============================================================================= +# Field of View +# ============================================================================= + +def update_entity_visibility(target_grid: mcrfpy.Grid) -> None: + global player, target_cursor + + for entity in target_grid.entities: + if entity == player: + entity.visible = True + continue + + if entity == target_cursor: + entity.visible = True + continue + + ex, ey = int(entity.grid_x), int(entity.grid_y) + entity.visible = target_grid.is_in_fov(ex, ey) + +def update_fov(target_grid: mcrfpy.Grid, target_fov_layer, player_x: int, player_y: int) -> None: + target_grid.compute_fov((player_x, player_y), radius=FOV_RADIUS, algorithm=mcrfpy.FOV.SHADOW) + + for y in range(GRID_HEIGHT): + for x in range(GRID_WIDTH): + if target_grid.is_in_fov(x, y): + mark_explored(x, y) + target_fov_layer.set((x, y), COLOR_VISIBLE) + elif is_explored(x, y): + target_fov_layer.set((x, y), COLOR_DISCOVERED) + else: + target_fov_layer.set((x, y), COLOR_UNKNOWN) + + update_entity_visibility(target_grid) + +# ============================================================================= +# Movement and Actions +# ============================================================================= + +def can_move_to(target_grid: mcrfpy.Grid, x: int, y: int, mover: mcrfpy.Entity = None) -> bool: + if x < 0 or x >= GRID_WIDTH or y < 0 or y >= GRID_HEIGHT: + return False + + if not target_grid.at(x, y).walkable: + return False + + blocker = get_blocking_entity_at(target_grid, x, y, exclude=mover) + if blocker is not None: + return False + + return True + +def try_move_or_attack(dx: int, dy: int) -> None: + global player, grid, fov_layer, game_over + + if game_over: + return + + px, py = int(player.grid_x), int(player.grid_y) + new_target_x = px + dx + new_target_y = py + dy + + if new_target_x < 0 or new_target_x >= GRID_WIDTH or new_target_y < 0 or new_target_y >= GRID_HEIGHT: + return + + blocker = get_blocking_entity_at(grid, new_target_x, new_target_y, exclude=player) + + if blocker is not None: + perform_attack(player, blocker) + enemy_turn() + elif grid.at(new_target_x, new_target_y).walkable: + player.grid_pos = (new_target_x, new_target_y) + update_fov(grid, fov_layer, new_target_x, new_target_y) + enemy_turn() + + update_ui() + +# ============================================================================= +# Enemy AI +# ============================================================================= + +def enemy_turn() -> None: + global player, grid, game_over + + if game_over: + return + + player_x, player_y = int(player.grid_x), int(player.grid_y) + + enemies = [] + for entity in grid.entities: + if entity == player: + continue + if entity in entity_data and entity_data[entity].is_alive: + enemies.append(entity) + + for enemy in enemies: + fighter = entity_data.get(enemy) + if fighter is None or not fighter.is_alive: + continue + + ex, ey = int(enemy.grid_x), int(enemy.grid_y) + + if not grid.is_in_fov(ex, ey): + continue + + dx = player_x - ex + dy = player_y - ey + + if abs(dx) <= 1 and abs(dy) <= 1 and (dx != 0 or dy != 0): + perform_attack(enemy, player) + else: + move_toward_player(enemy, ex, ey, player_x, player_y) + +def move_toward_player(enemy: mcrfpy.Entity, ex: int, ey: int, px: int, py: int) -> None: + global grid + + dx = 0 + dy = 0 + + if px < ex: + dx = -1 + elif px > ex: + dx = 1 + + if py < ey: + dy = -1 + elif py > ey: + dy = 1 + + new_x = ex + dx + new_y = ey + dy + + if can_move_to(grid, new_x, new_y, enemy): + enemy.grid_pos = (new_x, new_y) + elif dx != 0 and can_move_to(grid, ex + dx, ey, enemy): + enemy.grid_pos = (ex + dx, ey) + elif dy != 0 and can_move_to(grid, ex, ey + dy, enemy): + enemy.grid_pos = (ex, ey + dy) + +# ============================================================================= +# UI Updates +# ============================================================================= + +def update_ui() -> None: + global player, health_bar, inventory_panel, player_inventory + + if player in entity_data: + fighter = entity_data[player] + health_bar.update(fighter.hp, fighter.max_hp) + + if player_inventory: + inventory_panel.update(player_inventory) + +# ============================================================================= +# New Game Generation +# ============================================================================= + +def generate_new_game() -> None: + """Generate a fresh dungeon with new player.""" + global player, player_inventory, grid, fov_layer, game_over + global entity_data, item_data, dungeon_level, game_mode + + # Reset state + game_over = False + game_mode = GameMode.NORMAL + dungeon_level = 1 + + entity_data.clear() + item_data.clear() + + while len(grid.entities) > 0: + grid.entities.pop() + + fill_with_walls(grid) + init_explored() + message_log.clear() + + rooms: list[RectangularRoom] = [] + + for _ in range(MAX_ROOMS): + room_width = random.randint(ROOM_MIN_SIZE, ROOM_MAX_SIZE) + room_height = random.randint(ROOM_MIN_SIZE, ROOM_MAX_SIZE) + x = random.randint(1, GRID_WIDTH - room_width - 2) + y = random.randint(1, GRID_HEIGHT - room_height - 2) + + new_room = RectangularRoom(x, y, room_width, room_height) + + overlaps = False + for other_room in rooms: + if new_room.intersects(other_room): + overlaps = True + break + + if overlaps: + continue + + carve_room(grid, new_room) + + if rooms: + carve_l_tunnel(grid, new_room.center, rooms[-1].center) + + rooms.append(new_room) + + if rooms: + new_x, new_y = rooms[0].center + else: + new_x, new_y = GRID_WIDTH // 2, GRID_HEIGHT // 2 + + player = mcrfpy.Entity( + grid_pos=(new_x, new_y), + texture=texture, + sprite_index=SPRITE_PLAYER + ) + grid.entities.append(player) + + entity_data[player] = Fighter( + hp=30, + max_hp=30, + attack=5, + defense=2, + name="Player", + is_player=True + ) + + player_inventory = Inventory(capacity=10) + + for i, room in enumerate(rooms): + if i == 0: + continue + spawn_enemies_in_room(grid, room, texture) + spawn_items_in_room(grid, room, texture) + + for y in range(GRID_HEIGHT): + for x in range(GRID_WIDTH): + fov_layer.set((x, y), COLOR_UNKNOWN) + + update_fov(grid, fov_layer, new_x, new_y) + + mode_display.update(game_mode) + update_ui() + +# ============================================================================= +# Game Setup +# ============================================================================= + +# Create the scene +scene = mcrfpy.Scene("game") + +# Load texture +texture = mcrfpy.Texture("assets/kenney_tinydungeon.png", 16, 16) + +# Create the grid +grid = mcrfpy.Grid( + pos=(20, GAME_AREA_Y), + size=(700, GAME_AREA_HEIGHT - 20), + grid_size=(GRID_WIDTH, GRID_HEIGHT), + texture=texture, + zoom=1.0 +) + +# Add FOV layer +fov_layer = mcrfpy.ColorLayer(z_index=-1) +grid.add_layer(fov_layer) +for y in range(GRID_HEIGHT): + for x in range(GRID_WIDTH): + fov_layer.set((x, y), COLOR_UNKNOWN) + +# Add grid to scene +scene.children.append(grid) + +# ============================================================================= +# Create UI Elements +# ============================================================================= + +# Title bar +title = mcrfpy.Caption( + pos=(20, 10), + text="Part 10: Save/Load" +) +title.fill_color = mcrfpy.Color(255, 255, 255) +title.font_size = 24 +scene.children.append(title) + +# Instructions +instructions = mcrfpy.Caption( + pos=(250, 15), + text="WASD:Move | F:Ranged | G:Pickup | Ctrl+S:Save | R:Restart" +) +instructions.fill_color = mcrfpy.Color(180, 180, 180) +instructions.font_size = 14 +scene.children.append(instructions) + +# Health Bar +health_bar = HealthBar( + x=730, + y=10, + width=280, + height=30 +) +health_bar.add_to_scene(scene) + +# Mode Display +mode_display = ModeDisplay(x=20, y=40) +mode_display.add_to_scene(scene) + +# Inventory Panel +inventory_panel = InventoryPanel( + x=730, + y=GAME_AREA_Y, + width=280, + height=150 +) +inventory_panel.add_to_scene(scene) + +# Message Log +message_log = MessageLog( + x=20, + y=768 - UI_BOTTOM_HEIGHT + 10, + width=990, + height=UI_BOTTOM_HEIGHT - 20, + max_messages=6 +) +message_log.add_to_scene(scene) + +# ============================================================================= +# Initialize Game (Load or New) +# ============================================================================= + +# Initialize explored array +init_explored() + +# Try to load existing save, otherwise generate new game +if has_save_file(): + message_log.add("Found saved game. Loading...", COLOR_INFO) + if not load_game(): + message_log.add("Failed to load. Starting new game.", COLOR_WARNING) + generate_new_game() + message_log.add("Welcome to the dungeon!", COLOR_INFO) +else: + generate_new_game() + message_log.add("Welcome to the dungeon!", COLOR_INFO) + message_log.add("Press Ctrl+S to save your progress.", COLOR_INFO) + +# ============================================================================= +# Input Handling +# ============================================================================= + +def handle_keys(key: mcrfpy.Key, action: mcrfpy.InputState) -> None: + global game_over, game_mode + + if action != mcrfpy.InputState.PRESSED: + return + + # Always allow restart + if key == mcrfpy.Key.R: + delete_save() + generate_new_game() + message_log.add("A new adventure begins!", COLOR_INFO) + return + + if key == mcrfpy.Key.ESCAPE: + if game_mode == GameMode.TARGETING: + exit_targeting_mode() + message_log.add("Targeting cancelled.", COLOR_INFO) + return + else: + # Save on quit + if not game_over: + save_game() + mcrfpy.exit() + return + + # Save game (Ctrl+S or just S when not moving) + if key == mcrfpy.Key.S and game_mode == GameMode.NORMAL: + # Check if this is meant to be a save (could add modifier check) + # For simplicity, we will use a dedicated save key + pass + + # Dedicated save with period key + if key == mcrfpy.Key.PERIOD and game_mode == GameMode.NORMAL and not game_over: + save_game() + return + + if game_over: + return + + # Handle input based on game mode + if game_mode == GameMode.TARGETING: + handle_targeting_input(key) + else: + handle_normal_input(key) + +ITEM_KEYS = { + mcrfpy.Key.NUM_1: 0, + mcrfpy.Key.NUM_2: 1, + mcrfpy.Key.NUM_3: 2, + mcrfpy.Key.NUM_4: 3, + mcrfpy.Key.NUM_5: 4, +} + +def handle_normal_input(key: mcrfpy.Key) -> None: + # Movement + if key == mcrfpy.Key.W or key == mcrfpy.Key.UP: + try_move_or_attack(0, -1) + elif key == mcrfpy.Key.S or key == mcrfpy.Key.DOWN: + try_move_or_attack(0, 1) + elif key == mcrfpy.Key.A or key == mcrfpy.Key.LEFT: + try_move_or_attack(-1, 0) + elif key == mcrfpy.Key.D or key == mcrfpy.Key.RIGHT: + try_move_or_attack(1, 0) + # Ranged attack + elif key == mcrfpy.Key.F: + enter_targeting_mode() + # Pickup + elif key == mcrfpy.Key.G or key == mcrfpy.Key.COMMA: + pickup_item() + # Use items + elif key in ITEM_KEYS: + index = ITEM_KEYS[key] + if use_item(index): + enemy_turn() + update_ui() + +def handle_targeting_input(key: mcrfpy.Key) -> None: + if key == mcrfpy.Key.UP or key == mcrfpy.Key.W: + move_cursor(0, -1) + elif key == mcrfpy.Key.DOWN or key == mcrfpy.Key.S: + move_cursor(0, 1) + elif key == mcrfpy.Key.LEFT or key == mcrfpy.Key.A: + move_cursor(-1, 0) + elif key == mcrfpy.Key.RIGHT or key == mcrfpy.Key.D: + move_cursor(1, 0) + elif key == mcrfpy.Key.ENTER or key == mcrfpy.Key.SPACE: + confirm_target() + +scene.on_key = handle_keys + +# ============================================================================= +# Start the Game +# ============================================================================= + +scene.activate() +print("Part 10 loaded! Press Period (.) to save, Escape saves and quits.") diff --git a/tests/snippets/281_part_11_levels.py b/tests/snippets/281_part_11_levels.py new file mode 100644 index 0000000..90863b2 --- /dev/null +++ b/tests/snippets/281_part_11_levels.py @@ -0,0 +1,1732 @@ +# mcrf: objects=[Caption,Color,ColorLayer,Entity,FOV,Frame,Grid,InputState,Key,Scene,Texture] verified=0.2.8-dev status=ok +"""McRogueFace Tutorial - Part 11: Multiple Dungeon Levels + +Add stairs and multiple dungeon levels with scaling difficulty. +""" +import mcrfpy +import random +import json +import os +from dataclasses import dataclass, field +from typing import Optional +from enum import Enum + +# ============================================================================= +# Constants +# ============================================================================= + +# Sprite indices for CP437 tileset +SPRITE_WALL = 35 # '#' - wall +SPRITE_FLOOR = 46 # '.' - floor +SPRITE_PLAYER = 64 # '@' - player +SPRITE_CORPSE = 37 # '%' - remains +SPRITE_POTION = 173 # Potion sprite +SPRITE_CURSOR = 88 # 'X' - targeting cursor +SPRITE_STAIRS_DOWN = 62 # '>' - stairs down + +# Enemy sprites +SPRITE_GOBLIN = 103 # 'g' +SPRITE_ORC = 111 # 'o' +SPRITE_TROLL = 116 # 't' + +# Grid dimensions +GRID_WIDTH = 50 +GRID_HEIGHT = 30 + +# Room generation parameters +ROOM_MIN_SIZE = 6 +ROOM_MAX_SIZE = 12 +MAX_ROOMS = 8 + +# FOV and targeting settings +FOV_RADIUS = 8 +RANGED_ATTACK_RANGE = 6 +RANGED_ATTACK_DAMAGE = 4 + +# Save file location +SAVE_FILE = "savegame.json" + +# Visibility colors +COLOR_VISIBLE = mcrfpy.Color(0, 0, 0, 0) +COLOR_DISCOVERED = mcrfpy.Color(0, 0, 40, 180) +COLOR_UNKNOWN = mcrfpy.Color(0, 0, 0, 255) + +# Message colors +COLOR_PLAYER_ATTACK = mcrfpy.Color(200, 200, 200) +COLOR_ENEMY_ATTACK = mcrfpy.Color(255, 150, 150) +COLOR_PLAYER_DEATH = mcrfpy.Color(255, 50, 50) +COLOR_ENEMY_DEATH = mcrfpy.Color(100, 255, 100) +COLOR_HEAL = mcrfpy.Color(100, 255, 100) +COLOR_PICKUP = mcrfpy.Color(100, 200, 255) +COLOR_INFO = mcrfpy.Color(100, 100, 255) +COLOR_WARNING = mcrfpy.Color(255, 200, 50) +COLOR_INVALID = mcrfpy.Color(255, 100, 100) +COLOR_RANGED = mcrfpy.Color(255, 255, 100) +COLOR_SAVE = mcrfpy.Color(100, 255, 200) +COLOR_DESCEND = mcrfpy.Color(200, 200, 255) + +# UI Layout constants +UI_TOP_HEIGHT = 60 +UI_BOTTOM_HEIGHT = 150 +GAME_AREA_Y = UI_TOP_HEIGHT +GAME_AREA_HEIGHT = 768 - UI_TOP_HEIGHT - UI_BOTTOM_HEIGHT + +# ============================================================================= +# Game Modes +# ============================================================================= + +class GameMode(Enum): + NORMAL = "normal" + TARGETING = "targeting" + +# ============================================================================= +# Fighter Component +# ============================================================================= + +@dataclass +class Fighter: + """Combat stats for an entity.""" + hp: int + max_hp: int + attack: int + defense: int + name: str + is_player: bool = False + + @property + def is_alive(self) -> bool: + return self.hp > 0 + + def take_damage(self, amount: int) -> int: + actual_damage = min(self.hp, amount) + self.hp -= actual_damage + return actual_damage + + def heal(self, amount: int) -> int: + actual_heal = min(self.max_hp - self.hp, amount) + self.hp += actual_heal + return actual_heal + + def to_dict(self) -> dict: + return { + "hp": self.hp, + "max_hp": self.max_hp, + "attack": self.attack, + "defense": self.defense, + "name": self.name, + "is_player": self.is_player + } + + @classmethod + def from_dict(cls, data: dict) -> "Fighter": + return cls( + hp=data["hp"], + max_hp=data["max_hp"], + attack=data["attack"], + defense=data["defense"], + name=data["name"], + is_player=data.get("is_player", False) + ) + +# ============================================================================= +# Item Component +# ============================================================================= + +@dataclass +class Item: + """Data for an item that can be picked up and used.""" + name: str + item_type: str + heal_amount: int = 0 + + def to_dict(self) -> dict: + return { + "name": self.name, + "item_type": self.item_type, + "heal_amount": self.heal_amount + } + + @classmethod + def from_dict(cls, data: dict) -> "Item": + return cls( + name=data["name"], + item_type=data["item_type"], + heal_amount=data.get("heal_amount", 0) + ) + +# ============================================================================= +# Inventory System +# ============================================================================= + +@dataclass +class Inventory: + """Container for items the player is carrying.""" + capacity: int = 10 + items: list = field(default_factory=list) + + def add(self, item: Item) -> bool: + if len(self.items) >= self.capacity: + return False + self.items.append(item) + return True + + def remove(self, index: int) -> Optional[Item]: + if 0 <= index < len(self.items): + return self.items.pop(index) + return None + + def get(self, index: int) -> Optional[Item]: + if 0 <= index < len(self.items): + return self.items[index] + return None + + def is_full(self) -> bool: + return len(self.items) >= self.capacity + + def count(self) -> int: + return len(self.items) + + def to_dict(self) -> dict: + return { + "capacity": self.capacity, + "items": [item.to_dict() for item in self.items] + } + + @classmethod + def from_dict(cls, data: dict) -> "Inventory": + inv = cls(capacity=data.get("capacity", 10)) + inv.items = [Item.from_dict(item_data) for item_data in data.get("items", [])] + return inv + +# ============================================================================= +# Templates +# ============================================================================= + +ITEM_TEMPLATES = { + "health_potion": { + "name": "Health Potion", + "sprite": SPRITE_POTION, + "item_type": "health_potion", + "heal_amount": 10 + }, + "greater_health_potion": { + "name": "Greater Health Potion", + "sprite": SPRITE_POTION, + "item_type": "health_potion", + "heal_amount": 20 + } +} + +ENEMY_TEMPLATES = { + "goblin": { + "sprite": SPRITE_GOBLIN, + "hp": 6, + "attack": 3, + "defense": 0 + }, + "orc": { + "sprite": SPRITE_ORC, + "hp": 10, + "attack": 4, + "defense": 1 + }, + "troll": { + "sprite": SPRITE_TROLL, + "hp": 16, + "attack": 6, + "defense": 2 + } +} + +# ============================================================================= +# Difficulty Scaling +# ============================================================================= + +def get_max_enemies_per_room(level: int) -> int: + """Get maximum enemies per room based on dungeon level.""" + return min(2 + level, 6) + +def get_max_items_per_room(level: int) -> int: + """Get maximum items per room based on dungeon level.""" + return min(1 + level // 2, 3) + +def get_enemy_weights(level: int) -> list[tuple[str, float]]: + """Get enemy spawn weights based on dungeon level. + + Returns list of (enemy_type, cumulative_weight) tuples. + """ + if level <= 2: + # Levels 1-2: Mostly goblins + return [("goblin", 0.8), ("orc", 0.95), ("troll", 1.0)] + elif level <= 4: + # Levels 3-4: More orcs + return [("goblin", 0.5), ("orc", 0.85), ("troll", 1.0)] + else: + # Level 5+: Dangerous mix + return [("goblin", 0.3), ("orc", 0.6), ("troll", 1.0)] + +def get_item_weights(level: int) -> list[tuple[str, float]]: + """Get item spawn weights based on dungeon level.""" + if level <= 2: + return [("health_potion", 1.0)] + else: + return [("health_potion", 0.7), ("greater_health_potion", 1.0)] + +# ============================================================================= +# Message Log System +# ============================================================================= + +class MessageLog: + """A message log that displays recent game messages with colors.""" + + def __init__(self, x: int, y: int, width: int, height: int, max_messages: int = 6): + self.x = x + self.y = y + self.width = width + self.height = height + self.max_messages = max_messages + self.messages: list[tuple[str, mcrfpy.Color]] = [] + self.captions: list[mcrfpy.Caption] = [] + + self.frame = mcrfpy.Frame( + pos=(x, y), + size=(width, height) + ) + self.frame.fill_color = mcrfpy.Color(20, 20, 30, 200) + self.frame.outline = 2 + self.frame.outline_color = mcrfpy.Color(80, 80, 100) + + line_height = 20 + for i in range(max_messages): + caption = mcrfpy.Caption( + pos=(x + 10, y + 5 + i * line_height), + text="" + ) + caption.font_size = 14 + caption.fill_color = mcrfpy.Color(200, 200, 200) + self.captions.append(caption) + + def add_to_scene(self, scene: mcrfpy.Scene) -> None: + scene.children.append(self.frame) + for caption in self.captions: + scene.children.append(caption) + + def add(self, text: str, color: mcrfpy.Color = None) -> None: + if color is None: + color = mcrfpy.Color(200, 200, 200) + + self.messages.append((text, color)) + + while len(self.messages) > self.max_messages: + self.messages.pop(0) + + self._refresh() + + def _refresh(self) -> None: + for i, caption in enumerate(self.captions): + if i < len(self.messages): + text, color = self.messages[i] + caption.text = text + caption.fill_color = color + else: + caption.text = "" + + def clear(self) -> None: + self.messages.clear() + self._refresh() + +# ============================================================================= +# Health Bar System +# ============================================================================= + +class HealthBar: + """A visual health bar using nested frames.""" + + def __init__(self, x: int, y: int, width: int, height: int): + self.x = x + self.y = y + self.width = width + self.height = height + self.max_hp = 30 + self.current_hp = 30 + + self.bg_frame = mcrfpy.Frame( + pos=(x, y), + size=(width, height) + ) + self.bg_frame.fill_color = mcrfpy.Color(80, 0, 0) + self.bg_frame.outline = 2 + self.bg_frame.outline_color = mcrfpy.Color(150, 150, 150) + + self.fg_frame = mcrfpy.Frame( + pos=(x + 2, y + 2), + size=(width - 4, height - 4) + ) + self.fg_frame.fill_color = mcrfpy.Color(0, 180, 0) + self.fg_frame.outline = 0 + + self.label = mcrfpy.Caption( + pos=(x + 5, y + 2), + text=f"HP: {self.current_hp}/{self.max_hp}" + ) + self.label.font_size = 16 + self.label.fill_color = mcrfpy.Color(255, 255, 255) + + def add_to_scene(self, scene: mcrfpy.Scene) -> None: + scene.children.append(self.bg_frame) + scene.children.append(self.fg_frame) + scene.children.append(self.label) + + def update(self, current_hp: int, max_hp: int) -> None: + self.current_hp = current_hp + self.max_hp = max_hp + + percent = max(0, current_hp / max_hp) if max_hp > 0 else 0 + + inner_width = self.width - 4 + self.fg_frame.resize(int(inner_width * percent), self.height - 4) + + self.label.text = f"HP: {current_hp}/{max_hp}" + + if percent > 0.6: + self.fg_frame.fill_color = mcrfpy.Color(0, 180, 0) + elif percent > 0.3: + self.fg_frame.fill_color = mcrfpy.Color(180, 180, 0) + else: + self.fg_frame.fill_color = mcrfpy.Color(180, 0, 0) + +# ============================================================================= +# Inventory Panel +# ============================================================================= + +class InventoryPanel: + """A panel displaying the player's inventory.""" + + def __init__(self, x: int, y: int, width: int, height: int): + self.x = x + self.y = y + self.width = width + self.height = height + self.captions: list[mcrfpy.Caption] = [] + + self.frame = mcrfpy.Frame( + pos=(x, y), + size=(width, height) + ) + self.frame.fill_color = mcrfpy.Color(20, 20, 30, 200) + self.frame.outline = 2 + self.frame.outline_color = mcrfpy.Color(80, 80, 100) + + self.title = mcrfpy.Caption( + pos=(x + 10, y + 5), + text="Inventory (G:pickup, 1-5:use)" + ) + self.title.font_size = 14 + self.title.fill_color = mcrfpy.Color(200, 200, 255) + + for i in range(5): + caption = mcrfpy.Caption( + pos=(x + 10, y + 25 + i * 18), + text="" + ) + caption.font_size = 13 + caption.fill_color = mcrfpy.Color(180, 180, 180) + self.captions.append(caption) + + def add_to_scene(self, scene: mcrfpy.Scene) -> None: + scene.children.append(self.frame) + scene.children.append(self.title) + for caption in self.captions: + scene.children.append(caption) + + def update(self, inventory: Inventory) -> None: + for i, caption in enumerate(self.captions): + if i < len(inventory.items): + item = inventory.items[i] + caption.text = f"{i+1}. {item.name}" + caption.fill_color = mcrfpy.Color(180, 180, 180) + else: + caption.text = f"{i+1}. ---" + caption.fill_color = mcrfpy.Color(80, 80, 80) + +# ============================================================================= +# Level Display +# ============================================================================= + +class LevelDisplay: + """Displays current dungeon level.""" + + def __init__(self, x: int, y: int): + self.caption = mcrfpy.Caption( + pos=(x, y), + text="Level: 1" + ) + self.caption.font_size = 18 + self.caption.fill_color = mcrfpy.Color(200, 200, 255) + + def add_to_scene(self, scene: mcrfpy.Scene) -> None: + scene.children.append(self.caption) + + def update(self, level: int) -> None: + self.caption.text = f"Dungeon Level: {level}" + +# ============================================================================= +# Mode Display +# ============================================================================= + +class ModeDisplay: + """Displays the current game mode.""" + + def __init__(self, x: int, y: int): + self.caption = mcrfpy.Caption( + pos=(x, y), + text="[NORMAL MODE]" + ) + self.caption.font_size = 16 + self.caption.fill_color = mcrfpy.Color(100, 255, 100) + + def add_to_scene(self, scene: mcrfpy.Scene) -> None: + scene.children.append(self.caption) + + def update(self, mode: GameMode) -> None: + if mode == GameMode.NORMAL: + self.caption.text = "[NORMAL] F:Ranged | >:Descend" + self.caption.fill_color = mcrfpy.Color(100, 255, 100) + elif mode == GameMode.TARGETING: + self.caption.text = "[TARGETING] Arrows:Move, Enter:Fire, Esc:Cancel" + self.caption.fill_color = mcrfpy.Color(255, 255, 100) + +# ============================================================================= +# Global State +# ============================================================================= + +entity_data: dict[mcrfpy.Entity, Fighter] = {} +item_data: dict[mcrfpy.Entity, Item] = {} + +player: Optional[mcrfpy.Entity] = None +player_inventory: Optional[Inventory] = None +grid: Optional[mcrfpy.Grid] = None +fov_layer = None +texture: Optional[mcrfpy.Texture] = None +game_over: bool = False +dungeon_level: int = 1 + +# Stairs position +stairs_position: tuple[int, int] = (0, 0) + +# Game mode state +game_mode: GameMode = GameMode.NORMAL +target_cursor: Optional[mcrfpy.Entity] = None +target_x: int = 0 +target_y: int = 0 + +# UI components +message_log: Optional[MessageLog] = None +health_bar: Optional[HealthBar] = None +inventory_panel: Optional[InventoryPanel] = None +mode_display: Optional[ModeDisplay] = None +level_display: Optional[LevelDisplay] = None + +# ============================================================================= +# Room Class +# ============================================================================= + +class RectangularRoom: + """A rectangular room with its position and size.""" + + def __init__(self, x: int, y: int, width: int, height: int): + self.x1 = x + self.y1 = y + self.x2 = x + width + self.y2 = y + height + + @property + def center(self) -> tuple[int, int]: + center_x = (self.x1 + self.x2) // 2 + center_y = (self.y1 + self.y2) // 2 + return center_x, center_y + + @property + def inner(self) -> tuple[slice, slice]: + return slice(self.x1 + 1, self.x2), slice(self.y1 + 1, self.y2) + + def intersects(self, other: "RectangularRoom") -> bool: + return ( + self.x1 <= other.x2 and + self.x2 >= other.x1 and + self.y1 <= other.y2 and + self.y2 >= other.y1 + ) + +# ============================================================================= +# Exploration Tracking +# ============================================================================= + +explored: list[list[bool]] = [] + +def init_explored() -> None: + global explored + explored = [[False for _ in range(GRID_WIDTH)] for _ in range(GRID_HEIGHT)] + +def mark_explored(x: int, y: int) -> None: + if 0 <= x < GRID_WIDTH and 0 <= y < GRID_HEIGHT: + explored[y][x] = True + +def is_explored(x: int, y: int) -> bool: + if 0 <= x < GRID_WIDTH and 0 <= y < GRID_HEIGHT: + return explored[y][x] + return False + +# ============================================================================= +# Dungeon Generation +# ============================================================================= + +def fill_with_walls(target_grid: mcrfpy.Grid) -> None: + for y in range(GRID_HEIGHT): + for x in range(GRID_WIDTH): + cell = target_grid.at(x, y) + cell.tilesprite = SPRITE_WALL + cell.walkable = False + cell.transparent = False + +def carve_room(target_grid: mcrfpy.Grid, room: RectangularRoom) -> None: + inner_x, inner_y = room.inner + for y in range(inner_y.start, inner_y.stop): + for x in range(inner_x.start, inner_x.stop): + if 0 <= x < GRID_WIDTH and 0 <= y < GRID_HEIGHT: + cell = target_grid.at(x, y) + cell.tilesprite = SPRITE_FLOOR + cell.walkable = True + cell.transparent = True + +def carve_tunnel_horizontal(target_grid: mcrfpy.Grid, x1: int, x2: int, y: int) -> None: + for x in range(min(x1, x2), max(x1, x2) + 1): + if 0 <= x < GRID_WIDTH and 0 <= y < GRID_HEIGHT: + cell = target_grid.at(x, y) + cell.tilesprite = SPRITE_FLOOR + cell.walkable = True + cell.transparent = True + +def carve_tunnel_vertical(target_grid: mcrfpy.Grid, y1: int, y2: int, x: int) -> None: + for y in range(min(y1, y2), max(y1, y2) + 1): + if 0 <= x < GRID_WIDTH and 0 <= y < GRID_HEIGHT: + cell = target_grid.at(x, y) + cell.tilesprite = SPRITE_FLOOR + cell.walkable = True + cell.transparent = True + +def carve_l_tunnel( + target_grid: mcrfpy.Grid, + start: tuple[int, int], + end: tuple[int, int] +) -> None: + x1, y1 = start + x2, y2 = end + + if random.random() < 0.5: + carve_tunnel_horizontal(target_grid, x1, x2, y1) + carve_tunnel_vertical(target_grid, y1, y2, x2) + else: + carve_tunnel_vertical(target_grid, y1, y2, x1) + carve_tunnel_horizontal(target_grid, x1, x2, y2) + +def place_stairs(target_grid: mcrfpy.Grid, x: int, y: int) -> None: + """Place stairs down at the given position.""" + global stairs_position + + cell = target_grid.at(x, y) + cell.tilesprite = SPRITE_STAIRS_DOWN + cell.walkable = True + cell.transparent = True + + stairs_position = (x, y) + +def generate_dungeon(target_grid: mcrfpy.Grid, level: int) -> tuple[int, int]: + """Generate a new dungeon level. + + Args: + target_grid: The grid to generate into + level: Current dungeon level (affects difficulty) + + Returns: + Player starting position (x, y) + """ + global stairs_position + + fill_with_walls(target_grid) + + rooms: list[RectangularRoom] = [] + + for _ in range(MAX_ROOMS): + room_width = random.randint(ROOM_MIN_SIZE, ROOM_MAX_SIZE) + room_height = random.randint(ROOM_MIN_SIZE, ROOM_MAX_SIZE) + x = random.randint(1, GRID_WIDTH - room_width - 2) + y = random.randint(1, GRID_HEIGHT - room_height - 2) + + new_room = RectangularRoom(x, y, room_width, room_height) + + overlaps = False + for other_room in rooms: + if new_room.intersects(other_room): + overlaps = True + break + + if overlaps: + continue + + carve_room(target_grid, new_room) + + if rooms: + carve_l_tunnel(target_grid, new_room.center, rooms[-1].center) + + rooms.append(new_room) + + # Place stairs in the last room + if rooms: + stairs_x, stairs_y = rooms[-1].center + place_stairs(target_grid, stairs_x, stairs_y) + + # Return starting position (first room center) + if rooms: + return rooms[0].center + else: + return GRID_WIDTH // 2, GRID_HEIGHT // 2 + +# ============================================================================= +# Entity Management +# ============================================================================= + +def spawn_enemy(target_grid: mcrfpy.Grid, x: int, y: int, enemy_type: str, tex: mcrfpy.Texture) -> mcrfpy.Entity: + template = ENEMY_TEMPLATES[enemy_type] + + enemy = mcrfpy.Entity( + grid_pos=(x, y), + texture=tex, + sprite_index=template["sprite"] + ) + enemy.visible = False + + target_grid.entities.append(enemy) + + entity_data[enemy] = Fighter( + hp=template["hp"], + max_hp=template["hp"], + attack=template["attack"], + defense=template["defense"], + name=enemy_type.capitalize(), + is_player=False + ) + + return enemy + +def spawn_enemies_in_room(target_grid: mcrfpy.Grid, room: RectangularRoom, tex: mcrfpy.Texture, level: int) -> None: + """Spawn enemies in a room with level-scaled difficulty.""" + max_enemies = get_max_enemies_per_room(level) + num_enemies = random.randint(0, max_enemies) + enemy_weights = get_enemy_weights(level) + + for _ in range(num_enemies): + inner_x, inner_y = room.inner + x = random.randint(inner_x.start, inner_x.stop - 1) + y = random.randint(inner_y.start, inner_y.stop - 1) + + if is_position_occupied(target_grid, x, y): + continue + + # Select enemy type based on weights + roll = random.random() + enemy_type = "goblin" + for etype, threshold in enemy_weights: + if roll < threshold: + enemy_type = etype + break + + spawn_enemy(target_grid, x, y, enemy_type, tex) + +def spawn_item(target_grid: mcrfpy.Grid, x: int, y: int, item_type: str, tex: mcrfpy.Texture) -> mcrfpy.Entity: + template = ITEM_TEMPLATES[item_type] + + item_entity = mcrfpy.Entity( + grid_pos=(x, y), + texture=tex, + sprite_index=template["sprite"] + ) + item_entity.visible = False + + target_grid.entities.append(item_entity) + + item_data[item_entity] = Item( + name=template["name"], + item_type=template["item_type"], + heal_amount=template.get("heal_amount", 0) + ) + + return item_entity + +def spawn_items_in_room(target_grid: mcrfpy.Grid, room: RectangularRoom, tex: mcrfpy.Texture, level: int) -> None: + """Spawn items in a room with level-scaled variety.""" + max_items = get_max_items_per_room(level) + num_items = random.randint(0, max_items) + item_weights = get_item_weights(level) + + for _ in range(num_items): + inner_x, inner_y = room.inner + x = random.randint(inner_x.start, inner_x.stop - 1) + y = random.randint(inner_y.start, inner_y.stop - 1) + + if is_position_occupied(target_grid, x, y): + continue + + # Select item type based on weights + roll = random.random() + item_type = "health_potion" + for itype, threshold in item_weights: + if roll < threshold: + item_type = itype + break + + spawn_item(target_grid, x, y, item_type, tex) + +def is_position_occupied(target_grid: mcrfpy.Grid, x: int, y: int) -> bool: + for entity in target_grid.entities: + if int(entity.grid_x) == x and int(entity.grid_y) == y: + return True + return False + +def get_item_at(target_grid: mcrfpy.Grid, x: int, y: int) -> Optional[mcrfpy.Entity]: + for entity in target_grid.entities: + if entity in item_data: + if int(entity.grid_x) == x and int(entity.grid_y) == y: + return entity + return None + +def get_blocking_entity_at(target_grid: mcrfpy.Grid, x: int, y: int, exclude: mcrfpy.Entity = None) -> Optional[mcrfpy.Entity]: + for entity in target_grid.entities: + if entity == exclude: + continue + if int(entity.grid_x) == x and int(entity.grid_y) == y: + if entity in entity_data and entity_data[entity].is_alive: + return entity + return None + +def remove_entity(target_grid: mcrfpy.Grid, entity: mcrfpy.Entity) -> None: + for e in target_grid.entities: + if e == entity: + target_grid.entities.remove(e) + break + if entity in entity_data: + del entity_data[entity] + +def remove_item_entity(target_grid: mcrfpy.Grid, entity: mcrfpy.Entity) -> None: + for e in target_grid.entities: + if e == entity: + target_grid.entities.remove(e) + break + if entity in item_data: + del item_data[entity] + +def clear_entities_except_player(target_grid: mcrfpy.Grid) -> None: + """Remove all entities except the player.""" + global entity_data, item_data + + entities_to_remove = [] + for entity in target_grid.entities: + if entity in entity_data and entity_data[entity].is_player: + continue + entities_to_remove.append(entity) + + for entity in entities_to_remove: + if entity in entity_data: + del entity_data[entity] + if entity in item_data: + del item_data[entity] + + for e in target_grid.entities: + if e == entity: + target_grid.entities.remove(e) + break + +# ============================================================================= +# Level Transition +# ============================================================================= + +def descend_stairs() -> bool: + """Attempt to descend stairs at player's position. + + Returns: + True if descended, False if no stairs here + """ + global player, dungeon_level, grid, fov_layer, stairs_position + + px, py = int(player.grid_x), int(player.grid_y) + + # Check if player is on stairs + if (px, py) != stairs_position: + message_log.add("There are no stairs here.", COLOR_INVALID) + return False + + # Descend to next level + dungeon_level += 1 + + # Clear current level's entities (except player) + clear_entities_except_player(grid) + + # Generate new dungeon + init_explored() + player_start = generate_dungeon(grid, dungeon_level) + + # Move player to starting position + player.grid_pos = player_start + + # Spawn enemies and items based on level + # We need to get the rooms - regenerate them + # For simplicity, spawn in all floor tiles + spawn_entities_for_level(grid, texture, dungeon_level) + + # Reset FOV + for y in range(GRID_HEIGHT): + for x in range(GRID_WIDTH): + fov_layer.set((x, y), COLOR_UNKNOWN) + + update_fov(grid, fov_layer, player_start[0], player_start[1]) + + message_log.add(f"You descend to level {dungeon_level}...", COLOR_DESCEND) + level_display.update(dungeon_level) + update_ui() + + return True + +def spawn_entities_for_level(target_grid: mcrfpy.Grid, tex: mcrfpy.Texture, level: int) -> None: + """Spawn enemies and items throughout the dungeon.""" + # Find all floor tiles and group them into rough areas + floor_tiles = [] + for y in range(GRID_HEIGHT): + for x in range(GRID_WIDTH): + cell = target_grid.at(x, y) + if cell.walkable and cell.tilesprite == SPRITE_FLOOR: + floor_tiles.append((x, y)) + + # Spawn enemies + max_enemies = get_max_enemies_per_room(level) * 3 # Approximate total + enemy_weights = get_enemy_weights(level) + + for _ in range(max_enemies): + if not floor_tiles: + break + + x, y = random.choice(floor_tiles) + + # Don't spawn on player or stairs + if (x, y) == (int(player.grid_x), int(player.grid_y)): + continue + if (x, y) == stairs_position: + continue + if is_position_occupied(target_grid, x, y): + continue + + roll = random.random() + enemy_type = "goblin" + for etype, threshold in enemy_weights: + if roll < threshold: + enemy_type = etype + break + + spawn_enemy(target_grid, x, y, enemy_type, tex) + + # Spawn items + max_items = get_max_items_per_room(level) * 2 + item_weights = get_item_weights(level) + + for _ in range(max_items): + if not floor_tiles: + break + + x, y = random.choice(floor_tiles) + + if (x, y) == (int(player.grid_x), int(player.grid_y)): + continue + if (x, y) == stairs_position: + continue + if is_position_occupied(target_grid, x, y): + continue + + roll = random.random() + item_type = "health_potion" + for itype, threshold in item_weights: + if roll < threshold: + item_type = itype + break + + spawn_item(target_grid, x, y, item_type, tex) + +# ============================================================================= +# Save/Load System +# ============================================================================= + +def save_game() -> bool: + """Save the current game state to a JSON file.""" + global player, player_inventory, grid, explored, dungeon_level, stairs_position + + try: + tiles = [] + for y in range(GRID_HEIGHT): + row = [] + for x in range(GRID_WIDTH): + cell = grid.at(x, y) + row.append({ + "tilesprite": cell.tilesprite, + "walkable": cell.walkable, + "transparent": cell.transparent + }) + tiles.append(row) + + enemies = [] + for entity in grid.entities: + if entity == player: + continue + if entity in entity_data: + fighter = entity_data[entity] + enemies.append({ + "x": int(entity.grid_x), + "y": int(entity.grid_y), + "type": fighter.name.lower(), + "fighter": fighter.to_dict() + }) + + items_on_ground = [] + for entity in grid.entities: + if entity in item_data: + item = item_data[entity] + items_on_ground.append({ + "x": int(entity.grid_x), + "y": int(entity.grid_y), + "item": item.to_dict() + }) + + save_data = { + "version": 2, + "dungeon_level": dungeon_level, + "stairs_position": list(stairs_position), + "player": { + "x": int(player.grid_x), + "y": int(player.grid_y), + "fighter": entity_data[player].to_dict(), + "inventory": player_inventory.to_dict() + }, + "tiles": tiles, + "explored": [[explored[y][x] for x in range(GRID_WIDTH)] for y in range(GRID_HEIGHT)], + "enemies": enemies, + "items": items_on_ground + } + + with open(SAVE_FILE, "w") as f: + json.dump(save_data, f, indent=2) + + message_log.add("Game saved successfully!", COLOR_SAVE) + return True + + except Exception as e: + message_log.add(f"Failed to save: {str(e)}", COLOR_INVALID) + return False + +def load_game() -> bool: + """Load a saved game from JSON file.""" + global player, player_inventory, grid, explored, dungeon_level + global entity_data, item_data, fov_layer, game_over, stairs_position + + if not os.path.exists(SAVE_FILE): + return False + + try: + with open(SAVE_FILE, "r") as f: + save_data = json.load(f) + + entity_data.clear() + item_data.clear() + + while len(grid.entities) > 0: + grid.entities.pop() + + dungeon_level = save_data.get("dungeon_level", 1) + stairs_position = tuple(save_data.get("stairs_position", [0, 0])) + + tiles = save_data["tiles"] + for y in range(GRID_HEIGHT): + for x in range(GRID_WIDTH): + cell = grid.at(x, y) + tile_data = tiles[y][x] + cell.tilesprite = tile_data["tilesprite"] + cell.walkable = tile_data["walkable"] + cell.transparent = tile_data["transparent"] + + global explored + explored_data = save_data["explored"] + explored = [[explored_data[y][x] for x in range(GRID_WIDTH)] for y in range(GRID_HEIGHT)] + + player_data = save_data["player"] + player = mcrfpy.Entity( + grid_pos=(player_data["x"], player_data["y"]), + texture=texture, + sprite_index=SPRITE_PLAYER + ) + grid.entities.append(player) + + entity_data[player] = Fighter.from_dict(player_data["fighter"]) + player_inventory = Inventory.from_dict(player_data["inventory"]) + + for enemy_data in save_data.get("enemies", []): + enemy_type = enemy_data["type"] + template = ENEMY_TEMPLATES.get(enemy_type, ENEMY_TEMPLATES["goblin"]) + + enemy = mcrfpy.Entity( + grid_pos=(enemy_data["x"], enemy_data["y"]), + texture=texture, + sprite_index=template["sprite"] + ) + enemy.visible = False + + grid.entities.append(enemy) + entity_data[enemy] = Fighter.from_dict(enemy_data["fighter"]) + + for item_entry in save_data.get("items", []): + item_type = item_entry["item"]["item_type"] + template = ITEM_TEMPLATES.get(item_type, ITEM_TEMPLATES["health_potion"]) + + item_entity = mcrfpy.Entity( + grid_pos=(item_entry["x"], item_entry["y"]), + texture=texture, + sprite_index=template["sprite"] + ) + item_entity.visible = False + + grid.entities.append(item_entity) + item_data[item_entity] = Item.from_dict(item_entry["item"]) + + for y in range(GRID_HEIGHT): + for x in range(GRID_WIDTH): + fov_layer.set((x, y), COLOR_UNKNOWN) + + update_fov(grid, fov_layer, int(player.grid_x), int(player.grid_y)) + + game_over = False + + message_log.add("Game loaded successfully!", COLOR_SAVE) + level_display.update(dungeon_level) + update_ui() + return True + + except Exception as e: + message_log.add(f"Failed to load: {str(e)}", COLOR_INVALID) + return False + +def delete_save() -> bool: + try: + if os.path.exists(SAVE_FILE): + os.remove(SAVE_FILE) + return True + except Exception: + return False + +def has_save_file() -> bool: + return os.path.exists(SAVE_FILE) + +# ============================================================================= +# Targeting System +# ============================================================================= + +def enter_targeting_mode() -> None: + global game_mode, target_cursor, target_x, target_y, player, grid, texture + + target_x = int(player.grid_x) + target_y = int(player.grid_y) + + target_cursor = mcrfpy.Entity( + grid_pos=(target_x, target_y), + texture=texture, + sprite_index=SPRITE_CURSOR + ) + grid.entities.append(target_cursor) + + game_mode = GameMode.TARGETING + + message_log.add("Targeting mode: Arrows to aim, Enter to fire, Esc to cancel.", COLOR_INFO) + mode_display.update(game_mode) + +def exit_targeting_mode() -> None: + global game_mode, target_cursor, grid + + if target_cursor is not None: + for e in grid.entities: + if e == target_cursor: + grid.entities.remove(e) + break + target_cursor = None + + game_mode = GameMode.NORMAL + mode_display.update(game_mode) + +def move_cursor(dx: int, dy: int) -> None: + global target_x, target_y, target_cursor, grid, player + + new_x = target_x + dx + new_y = target_y + dy + + if new_x < 0 or new_x >= GRID_WIDTH or new_y < 0 or new_y >= GRID_HEIGHT: + return + + if not grid.is_in_fov(new_x, new_y): + message_log.add("You cannot see that location.", COLOR_INVALID) + return + + player_x, player_y = int(player.grid_x), int(player.grid_y) + distance = abs(new_x - player_x) + abs(new_y - player_y) + if distance > RANGED_ATTACK_RANGE: + message_log.add(f"Target is out of range (max {RANGED_ATTACK_RANGE}).", COLOR_WARNING) + return + + target_x = new_x + target_y = new_y + target_cursor.x = target_x + target_cursor.y = target_y + + enemy = get_blocking_entity_at(grid, target_x, target_y, exclude=player) + if enemy and enemy in entity_data: + fighter = entity_data[enemy] + message_log.add(f"Target: {fighter.name} (HP: {fighter.hp}/{fighter.max_hp})", COLOR_RANGED) + +def confirm_target() -> None: + global game_mode, target_x, target_y, player, grid + + if target_x == int(player.grid_x) and target_y == int(player.grid_y): + message_log.add("You cannot target yourself!", COLOR_INVALID) + return + + target_enemy = get_blocking_entity_at(grid, target_x, target_y, exclude=player) + + if target_enemy is None or target_enemy not in entity_data: + message_log.add("No valid target at that location.", COLOR_INVALID) + return + + perform_ranged_attack(target_enemy) + exit_targeting_mode() + enemy_turn() + update_ui() + +def perform_ranged_attack(target_entity: mcrfpy.Entity) -> None: + global player, game_over + + defender = entity_data.get(target_entity) + attacker = entity_data.get(player) + + if defender is None or attacker is None: + return + + damage = max(1, RANGED_ATTACK_DAMAGE - defender.defense // 2) + + defender.take_damage(damage) + + message_log.add( + f"Your ranged attack hits the {defender.name} for {damage} damage!", + COLOR_RANGED + ) + + if not defender.is_alive: + handle_death(target_entity, defender) + +# ============================================================================= +# Combat System +# ============================================================================= + +def calculate_damage(attacker: Fighter, defender: Fighter) -> int: + return max(0, attacker.attack - defender.defense) + +def perform_attack(attacker_entity: mcrfpy.Entity, defender_entity: mcrfpy.Entity) -> None: + global game_over + + attacker = entity_data.get(attacker_entity) + defender = entity_data.get(defender_entity) + + if attacker is None or defender is None: + return + + damage = calculate_damage(attacker, defender) + defender.take_damage(damage) + + if damage > 0: + if attacker.is_player: + message_log.add( + f"You hit the {defender.name} for {damage} damage!", + COLOR_PLAYER_ATTACK + ) + else: + message_log.add( + f"The {attacker.name} hits you for {damage} damage!", + COLOR_ENEMY_ATTACK + ) + else: + if attacker.is_player: + message_log.add( + f"You hit the {defender.name} but deal no damage.", + mcrfpy.Color(150, 150, 150) + ) + else: + message_log.add( + f"The {attacker.name} hits but deals no damage.", + mcrfpy.Color(150, 150, 200) + ) + + if not defender.is_alive: + handle_death(defender_entity, defender) + + update_ui() + +def handle_death(entity: mcrfpy.Entity, fighter: Fighter) -> None: + global game_over, grid + + if fighter.is_player: + message_log.add("You have died!", COLOR_PLAYER_DEATH) + message_log.add("Press R to restart or Escape to quit.", COLOR_INFO) + game_over = True + entity.sprite_index = SPRITE_CORPSE + delete_save() + else: + message_log.add(f"The {fighter.name} dies!", COLOR_ENEMY_DEATH) + remove_entity(grid, entity) + +# ============================================================================= +# Item Actions +# ============================================================================= + +def pickup_item() -> bool: + global player, player_inventory, grid + + px, py = int(player.grid_x), int(player.grid_y) + item_entity = get_item_at(grid, px, py) + + if item_entity is None: + message_log.add("There is nothing to pick up here.", COLOR_INVALID) + return False + + if player_inventory.is_full(): + message_log.add("Your inventory is full!", COLOR_WARNING) + return False + + item = item_data.get(item_entity) + if item is None: + return False + + player_inventory.add(item) + remove_item_entity(grid, item_entity) + + message_log.add(f"You pick up the {item.name}.", COLOR_PICKUP) + + update_ui() + return True + +def use_item(index: int) -> bool: + global player, player_inventory + + item = player_inventory.get(index) + if item is None: + message_log.add("Invalid item selection.", COLOR_INVALID) + return False + + if item.item_type == "health_potion": + fighter = entity_data.get(player) + if fighter is None: + return False + + if fighter.hp >= fighter.max_hp: + message_log.add("You are already at full health!", COLOR_WARNING) + return False + + actual_heal = fighter.heal(item.heal_amount) + player_inventory.remove(index) + + message_log.add(f"You drink the {item.name} and recover {actual_heal} HP!", COLOR_HEAL) + + update_ui() + return True + + message_log.add(f"You cannot use the {item.name}.", COLOR_INVALID) + return False + +# ============================================================================= +# Field of View +# ============================================================================= + +def update_entity_visibility(target_grid: mcrfpy.Grid) -> None: + global player, target_cursor + + for entity in target_grid.entities: + if entity == player: + entity.visible = True + continue + + if entity == target_cursor: + entity.visible = True + continue + + ex, ey = int(entity.grid_x), int(entity.grid_y) + entity.visible = target_grid.is_in_fov(ex, ey) + +def update_fov(target_grid: mcrfpy.Grid, target_fov_layer, player_x: int, player_y: int) -> None: + target_grid.compute_fov((player_x, player_y), radius=FOV_RADIUS, algorithm=mcrfpy.FOV.SHADOW) + + for y in range(GRID_HEIGHT): + for x in range(GRID_WIDTH): + if target_grid.is_in_fov(x, y): + mark_explored(x, y) + target_fov_layer.set((x, y), COLOR_VISIBLE) + elif is_explored(x, y): + target_fov_layer.set((x, y), COLOR_DISCOVERED) + else: + target_fov_layer.set((x, y), COLOR_UNKNOWN) + + update_entity_visibility(target_grid) + +# ============================================================================= +# Movement and Actions +# ============================================================================= + +def can_move_to(target_grid: mcrfpy.Grid, x: int, y: int, mover: mcrfpy.Entity = None) -> bool: + if x < 0 or x >= GRID_WIDTH or y < 0 or y >= GRID_HEIGHT: + return False + + if not target_grid.at(x, y).walkable: + return False + + blocker = get_blocking_entity_at(target_grid, x, y, exclude=mover) + if blocker is not None: + return False + + return True + +def try_move_or_attack(dx: int, dy: int) -> None: + global player, grid, fov_layer, game_over + + if game_over: + return + + px, py = int(player.grid_x), int(player.grid_y) + new_target_x = px + dx + new_target_y = py + dy + + if new_target_x < 0 or new_target_x >= GRID_WIDTH or new_target_y < 0 or new_target_y >= GRID_HEIGHT: + return + + blocker = get_blocking_entity_at(grid, new_target_x, new_target_y, exclude=player) + + if blocker is not None: + perform_attack(player, blocker) + enemy_turn() + elif grid.at(new_target_x, new_target_y).walkable: + player.grid_x = new_target_x + player.grid_y = new_target_y + update_fov(grid, fov_layer, new_target_x, new_target_y) + enemy_turn() + + update_ui() + +# ============================================================================= +# Enemy AI +# ============================================================================= + +def enemy_turn() -> None: + global player, grid, game_over + + if game_over: + return + + player_x, player_y = int(player.grid_x), int(player.grid_y) + + enemies = [] + for entity in grid.entities: + if entity == player: + continue + if entity in entity_data and entity_data[entity].is_alive: + enemies.append(entity) + + for enemy in enemies: + fighter = entity_data.get(enemy) + if fighter is None or not fighter.is_alive: + continue + + ex, ey = int(enemy.x), int(enemy.y) + + if not grid.is_in_fov(ex, ey): + continue + + dx = player_x - ex + dy = player_y - ey + + if abs(dx) <= 1 and abs(dy) <= 1 and (dx != 0 or dy != 0): + perform_attack(enemy, player) + else: + move_toward_player(enemy, ex, ey, player_x, player_y) + +def move_toward_player(enemy: mcrfpy.Entity, ex: int, ey: int, px: int, py: int) -> None: + global grid + + dx = 0 + dy = 0 + + if px < ex: + dx = -1 + elif px > ex: + dx = 1 + + if py < ey: + dy = -1 + elif py > ey: + dy = 1 + + new_x = ex + dx + new_y = ey + dy + + if can_move_to(grid, new_x, new_y, enemy): + enemy.x = new_x + enemy.y = new_y + elif dx != 0 and can_move_to(grid, ex + dx, ey, enemy): + enemy.x = ex + dx + elif dy != 0 and can_move_to(grid, ex, ey + dy, enemy): + enemy.y = ey + dy + +# ============================================================================= +# UI Updates +# ============================================================================= + +def update_ui() -> None: + global player, health_bar, inventory_panel, player_inventory + + if player in entity_data: + fighter = entity_data[player] + health_bar.update(fighter.hp, fighter.max_hp) + + if player_inventory: + inventory_panel.update(player_inventory) + +# ============================================================================= +# New Game Generation +# ============================================================================= + +def generate_new_game() -> None: + """Generate a fresh dungeon with new player.""" + global player, player_inventory, grid, fov_layer, game_over + global entity_data, item_data, dungeon_level, game_mode + + game_over = False + game_mode = GameMode.NORMAL + dungeon_level = 1 + + entity_data.clear() + item_data.clear() + + while len(grid.entities) > 0: + grid.entities.pop() + + init_explored() + message_log.clear() + + player_start = generate_dungeon(grid, dungeon_level) + + player = mcrfpy.Entity( + grid_pos=(player_start[0], player_start[1]), + texture=texture, + sprite_index=SPRITE_PLAYER + ) + grid.entities.append(player) + + entity_data[player] = Fighter( + hp=30, + max_hp=30, + attack=5, + defense=2, + name="Player", + is_player=True + ) + + player_inventory = Inventory(capacity=10) + + spawn_entities_for_level(grid, texture, dungeon_level) + + for y in range(GRID_HEIGHT): + for x in range(GRID_WIDTH): + fov_layer.set((x, y), COLOR_UNKNOWN) + + update_fov(grid, fov_layer, player_start[0], player_start[1]) + + mode_display.update(game_mode) + level_display.update(dungeon_level) + update_ui() + +# ============================================================================= +# Game Setup +# ============================================================================= + +scene = mcrfpy.Scene("game") +texture = mcrfpy.Texture("assets/kenney_tinydungeon.png", 16, 16) + +grid = mcrfpy.Grid( + pos=(20, GAME_AREA_Y), + size=(700, GAME_AREA_HEIGHT - 20), + grid_size=(GRID_WIDTH, GRID_HEIGHT), + texture=texture, + zoom=1.0 +) + +fov_layer = mcrfpy.ColorLayer(z_index=-1) +grid.add_layer(fov_layer) +for y in range(GRID_HEIGHT): + for x in range(GRID_WIDTH): + fov_layer.set((x, y), COLOR_UNKNOWN) + +scene.children.append(grid) + +# ============================================================================= +# Create UI Elements +# ============================================================================= + +title = mcrfpy.Caption( + pos=(20, 10), + text="Part 11: Multiple Levels" +) +title.fill_color = mcrfpy.Color(255, 255, 255) +title.font_size = 24 +scene.children.append(title) + +instructions = mcrfpy.Caption( + pos=(260, 15), + text="WASD:Move | >:Descend | F:Ranged | G:Pickup | R:Restart" +) +instructions.fill_color = mcrfpy.Color(180, 180, 180) +instructions.font_size = 14 +scene.children.append(instructions) + +health_bar = HealthBar(x=730, y=10, width=280, height=30) +health_bar.add_to_scene(scene) + +level_display = LevelDisplay(x=730, y=45) +level_display.add_to_scene(scene) + +mode_display = ModeDisplay(x=20, y=40) +mode_display.add_to_scene(scene) + +inventory_panel = InventoryPanel(x=730, y=GAME_AREA_Y, width=280, height=150) +inventory_panel.add_to_scene(scene) + +message_log = MessageLog( + x=20, + y=768 - UI_BOTTOM_HEIGHT + 10, + width=990, + height=UI_BOTTOM_HEIGHT - 20, + max_messages=6 +) +message_log.add_to_scene(scene) + +# ============================================================================= +# Initialize Game +# ============================================================================= + +init_explored() + +if has_save_file(): + message_log.add("Found saved game. Loading...", COLOR_INFO) + if not load_game(): + message_log.add("Failed to load. Starting new game.", COLOR_WARNING) + generate_new_game() + message_log.add("Welcome to the dungeon!", COLOR_INFO) +else: + generate_new_game() + message_log.add("Welcome to the dungeon!", COLOR_INFO) + message_log.add("Find the stairs (>) to descend deeper.", COLOR_INFO) + +# ============================================================================= +# Input Handling +# ============================================================================= + +def handle_keys(key: mcrfpy.Key, action: mcrfpy.InputState) -> None: + global game_over, game_mode + + if action != mcrfpy.InputState.PRESSED: + return + + if key == mcrfpy.Key.R: + delete_save() + generate_new_game() + message_log.add("A new adventure begins!", COLOR_INFO) + return + + if key == mcrfpy.Key.ESCAPE: + if game_mode == GameMode.TARGETING: + exit_targeting_mode() + message_log.add("Targeting cancelled.", COLOR_INFO) + return + else: + if not game_over: + save_game() + mcrfpy.exit() + return + + if key == mcrfpy.Key.PERIOD and game_mode == GameMode.NORMAL and not game_over: + save_game() + return + + if game_over: + return + + if game_mode == GameMode.TARGETING: + handle_targeting_input(key) + else: + handle_normal_input(key) + +ITEM_KEYS = { + mcrfpy.Key.NUM_1: 0, + mcrfpy.Key.NUM_2: 1, + mcrfpy.Key.NUM_3: 2, + mcrfpy.Key.NUM_4: 3, + mcrfpy.Key.NUM_5: 4, +} + +def handle_normal_input(key: mcrfpy.Key) -> None: + if key == mcrfpy.Key.W or key == mcrfpy.Key.UP: + try_move_or_attack(0, -1) + elif key == mcrfpy.Key.S or key == mcrfpy.Key.DOWN: + try_move_or_attack(0, 1) + elif key == mcrfpy.Key.A or key == mcrfpy.Key.LEFT: + try_move_or_attack(-1, 0) + elif key == mcrfpy.Key.D or key == mcrfpy.Key.RIGHT: + try_move_or_attack(1, 0) + elif key == mcrfpy.Key.F: + enter_targeting_mode() + elif key == mcrfpy.Key.G or key == mcrfpy.Key.COMMA: + pickup_item() + elif key == mcrfpy.Key.PERIOD and mcrfpy.keyboard.shift: + # Shift+. (>) to descend stairs + descend_stairs() + elif key in ITEM_KEYS: + index = ITEM_KEYS[key] + if use_item(index): + enemy_turn() + update_ui() + +def handle_targeting_input(key: mcrfpy.Key) -> None: + if key == mcrfpy.Key.UP or key == mcrfpy.Key.W: + move_cursor(0, -1) + elif key == mcrfpy.Key.DOWN or key == mcrfpy.Key.S: + move_cursor(0, 1) + elif key == mcrfpy.Key.LEFT or key == mcrfpy.Key.A: + move_cursor(-1, 0) + elif key == mcrfpy.Key.RIGHT or key == mcrfpy.Key.D: + move_cursor(1, 0) + elif key == mcrfpy.Key.ENTER or key == mcrfpy.Key.SPACE: + confirm_target() + +scene.on_key = handle_keys + +# ============================================================================= +# Start the Game +# ============================================================================= + +scene.activate() +print("Part 11 loaded! Find stairs (>) to descend to deeper levels.") diff --git a/tests/snippets/282_part_13_equipment.py b/tests/snippets/282_part_13_equipment.py new file mode 100644 index 0000000..0f0ecd9 --- /dev/null +++ b/tests/snippets/282_part_13_equipment.py @@ -0,0 +1,1791 @@ +# mcrf: objects=[Caption,Color,ColorLayer,Entity,FOV,Frame,Grid,InputState,Key,Scene,Texture] verified=0.2.8-dev status=ok +"""McRogueFace Tutorial - Part 13: Equipment System + +Add weapons and armor with stat bonuses. +""" +import mcrfpy +import random +import json +import os +from dataclasses import dataclass, field +from typing import Optional +from enum import Enum + +# ============================================================================= +# Constants +# ============================================================================= + +# Sprite indices for CP437 tileset +SPRITE_WALL = 35 # '#' - wall +SPRITE_FLOOR = 46 # '.' - floor +SPRITE_PLAYER = 64 # '@' - player +SPRITE_CORPSE = 37 # '%' - remains +SPRITE_POTION = 173 # Potion sprite +SPRITE_CURSOR = 88 # 'X' - targeting cursor +SPRITE_STAIRS_DOWN = 62 # '>' - stairs down +SPRITE_SWORD = 47 # '/' - weapon +SPRITE_ARMOR = 91 # '[' - armor + +# Enemy sprites +SPRITE_GOBLIN = 103 # 'g' +SPRITE_ORC = 111 # 'o' +SPRITE_TROLL = 116 # 't' + +# Grid dimensions +GRID_WIDTH = 50 +GRID_HEIGHT = 30 + +# Room generation parameters +ROOM_MIN_SIZE = 6 +ROOM_MAX_SIZE = 12 +MAX_ROOMS = 8 + +# FOV and targeting settings +FOV_RADIUS = 8 +RANGED_ATTACK_RANGE = 6 +RANGED_ATTACK_DAMAGE = 4 + +# Save file location +SAVE_FILE = "savegame.json" + +# XP values for enemies +ENEMY_XP_VALUES = { + "goblin": 35, + "orc": 50, + "troll": 100 +} + +# Visibility colors +COLOR_VISIBLE = mcrfpy.Color(0, 0, 0, 0) +COLOR_DISCOVERED = mcrfpy.Color(0, 0, 40, 180) +COLOR_UNKNOWN = mcrfpy.Color(0, 0, 0, 255) + +# Message colors +COLOR_PLAYER_ATTACK = mcrfpy.Color(200, 200, 200) +COLOR_ENEMY_ATTACK = mcrfpy.Color(255, 150, 150) +COLOR_PLAYER_DEATH = mcrfpy.Color(255, 50, 50) +COLOR_ENEMY_DEATH = mcrfpy.Color(100, 255, 100) +COLOR_HEAL = mcrfpy.Color(100, 255, 100) +COLOR_PICKUP = mcrfpy.Color(100, 200, 255) +COLOR_INFO = mcrfpy.Color(100, 100, 255) +COLOR_WARNING = mcrfpy.Color(255, 200, 50) +COLOR_INVALID = mcrfpy.Color(255, 100, 100) +COLOR_RANGED = mcrfpy.Color(255, 255, 100) +COLOR_SAVE = mcrfpy.Color(100, 255, 200) +COLOR_DESCEND = mcrfpy.Color(200, 200, 255) +COLOR_LEVEL_UP = mcrfpy.Color(255, 255, 100) +COLOR_XP = mcrfpy.Color(200, 200, 100) +COLOR_EQUIP = mcrfpy.Color(150, 200, 255) + +# UI Layout constants +UI_TOP_HEIGHT = 60 +UI_BOTTOM_HEIGHT = 150 +GAME_AREA_Y = UI_TOP_HEIGHT +GAME_AREA_HEIGHT = 768 - UI_TOP_HEIGHT - UI_BOTTOM_HEIGHT + +# ============================================================================= +# Game Modes +# ============================================================================= + +class GameMode(Enum): + NORMAL = "normal" + TARGETING = "targeting" + +# ============================================================================= +# Equipment Component +# ============================================================================= + +@dataclass +class Equipment: + """An equippable item that provides stat bonuses.""" + name: str + slot: str # "weapon" or "armor" + attack_bonus: int = 0 + defense_bonus: int = 0 + + def to_dict(self) -> dict: + return { + "name": self.name, + "slot": self.slot, + "attack_bonus": self.attack_bonus, + "defense_bonus": self.defense_bonus + } + + @classmethod + def from_dict(cls, data: dict) -> "Equipment": + return cls( + name=data["name"], + slot=data["slot"], + attack_bonus=data.get("attack_bonus", 0), + defense_bonus=data.get("defense_bonus", 0) + ) + +# ============================================================================= +# Fighter Component with Equipment +# ============================================================================= + +@dataclass +class Fighter: + """Combat stats for an entity with experience and equipment.""" + hp: int + max_hp: int + base_attack: int + base_defense: int + name: str + is_player: bool = False + xp: int = 0 + level: int = 1 + weapon: Optional[Equipment] = None + armor: Optional[Equipment] = None + + @property + def attack(self) -> int: + """Total attack including equipment bonus.""" + bonus = self.weapon.attack_bonus if self.weapon else 0 + return self.base_attack + bonus + + @property + def defense(self) -> int: + """Total defense including equipment bonus.""" + bonus = self.armor.defense_bonus if self.armor else 0 + return self.base_defense + bonus + + @property + def is_alive(self) -> bool: + return self.hp > 0 + + @property + def xp_to_next_level(self) -> int: + return self.level * 100 + + @property + def xp_progress(self) -> float: + return self.xp / self.xp_to_next_level if self.xp_to_next_level > 0 else 0 + + def take_damage(self, amount: int) -> int: + actual_damage = min(self.hp, amount) + self.hp -= actual_damage + return actual_damage + + def heal(self, amount: int) -> int: + actual_heal = min(self.max_hp - self.hp, amount) + self.hp += actual_heal + return actual_heal + + def gain_xp(self, amount: int) -> bool: + self.xp += amount + if self.xp >= self.xp_to_next_level: + self.level_up() + return True + return False + + def level_up(self) -> None: + self.xp -= self.xp_to_next_level + self.level += 1 + hp_increase = 5 + attack_increase = 1 + defense_increase = 1 if self.level % 3 == 0 else 0 + self.max_hp += hp_increase + self.hp = self.max_hp + self.base_attack += attack_increase + self.base_defense += defense_increase + + def equip(self, equipment: Equipment) -> Optional[Equipment]: + """Equip an item, returning any previously equipped item. + + Args: + equipment: The equipment to equip + + Returns: + Previously equipped item in that slot, or None + """ + old_equipment = None + + if equipment.slot == "weapon": + old_equipment = self.weapon + self.weapon = equipment + elif equipment.slot == "armor": + old_equipment = self.armor + self.armor = equipment + + return old_equipment + + def unequip(self, slot: str) -> Optional[Equipment]: + """Unequip an item from a slot. + + Args: + slot: "weapon" or "armor" + + Returns: + The unequipped item, or None if slot was empty + """ + if slot == "weapon": + item = self.weapon + self.weapon = None + return item + elif slot == "armor": + item = self.armor + self.armor = None + return item + return None + + def to_dict(self) -> dict: + return { + "hp": self.hp, + "max_hp": self.max_hp, + "base_attack": self.base_attack, + "base_defense": self.base_defense, + "name": self.name, + "is_player": self.is_player, + "xp": self.xp, + "level": self.level, + "weapon": self.weapon.to_dict() if self.weapon else None, + "armor": self.armor.to_dict() if self.armor else None + } + + @classmethod + def from_dict(cls, data: dict) -> "Fighter": + fighter = cls( + hp=data["hp"], + max_hp=data["max_hp"], + base_attack=data.get("base_attack", data.get("attack", 5)), + base_defense=data.get("base_defense", data.get("defense", 2)), + name=data["name"], + is_player=data.get("is_player", False), + xp=data.get("xp", 0), + level=data.get("level", 1) + ) + if data.get("weapon"): + fighter.weapon = Equipment.from_dict(data["weapon"]) + if data.get("armor"): + fighter.armor = Equipment.from_dict(data["armor"]) + return fighter + +# ============================================================================= +# Item Component +# ============================================================================= + +@dataclass +class Item: + """Data for an item that can be picked up and used.""" + name: str + item_type: str + heal_amount: int = 0 + equipment: Optional[Equipment] = None + + def to_dict(self) -> dict: + result = { + "name": self.name, + "item_type": self.item_type, + "heal_amount": self.heal_amount + } + if self.equipment: + result["equipment"] = self.equipment.to_dict() + return result + + @classmethod + def from_dict(cls, data: dict) -> "Item": + item = cls( + name=data["name"], + item_type=data["item_type"], + heal_amount=data.get("heal_amount", 0) + ) + if data.get("equipment"): + item.equipment = Equipment.from_dict(data["equipment"]) + return item + +# ============================================================================= +# Inventory System +# ============================================================================= + +@dataclass +class Inventory: + """Container for items the player is carrying.""" + capacity: int = 10 + items: list = field(default_factory=list) + + def add(self, item: Item) -> bool: + if len(self.items) >= self.capacity: + return False + self.items.append(item) + return True + + def remove(self, index: int) -> Optional[Item]: + if 0 <= index < len(self.items): + return self.items.pop(index) + return None + + def get(self, index: int) -> Optional[Item]: + if 0 <= index < len(self.items): + return self.items[index] + return None + + def is_full(self) -> bool: + return len(self.items) >= self.capacity + + def count(self) -> int: + return len(self.items) + + def to_dict(self) -> dict: + return { + "capacity": self.capacity, + "items": [item.to_dict() for item in self.items] + } + + @classmethod + def from_dict(cls, data: dict) -> "Inventory": + inv = cls(capacity=data.get("capacity", 10)) + inv.items = [Item.from_dict(item_data) for item_data in data.get("items", [])] + return inv + +# ============================================================================= +# Templates +# ============================================================================= + +ITEM_TEMPLATES = { + "health_potion": { + "name": "Health Potion", + "sprite": SPRITE_POTION, + "item_type": "health_potion", + "heal_amount": 10 + }, + "greater_health_potion": { + "name": "Greater Health Potion", + "sprite": SPRITE_POTION, + "item_type": "health_potion", + "heal_amount": 20 + }, + # Weapons + "dagger": { + "name": "Dagger", + "sprite": SPRITE_SWORD, + "item_type": "equipment", + "slot": "weapon", + "attack_bonus": 2, + "defense_bonus": 0 + }, + "sword": { + "name": "Sword", + "sprite": SPRITE_SWORD, + "item_type": "equipment", + "slot": "weapon", + "attack_bonus": 4, + "defense_bonus": 0 + }, + "great_axe": { + "name": "Great Axe", + "sprite": SPRITE_SWORD, + "item_type": "equipment", + "slot": "weapon", + "attack_bonus": 6, + "defense_bonus": -1 + }, + # Armor + "leather_armor": { + "name": "Leather Armor", + "sprite": SPRITE_ARMOR, + "item_type": "equipment", + "slot": "armor", + "attack_bonus": 0, + "defense_bonus": 2 + }, + "chain_mail": { + "name": "Chain Mail", + "sprite": SPRITE_ARMOR, + "item_type": "equipment", + "slot": "armor", + "attack_bonus": 0, + "defense_bonus": 4 + }, + "plate_armor": { + "name": "Plate Armor", + "sprite": SPRITE_ARMOR, + "item_type": "equipment", + "slot": "armor", + "attack_bonus": -1, + "defense_bonus": 6 + } +} + +ENEMY_TEMPLATES = { + "goblin": { + "sprite": SPRITE_GOBLIN, + "hp": 6, + "attack": 3, + "defense": 0 + }, + "orc": { + "sprite": SPRITE_ORC, + "hp": 10, + "attack": 4, + "defense": 1 + }, + "troll": { + "sprite": SPRITE_TROLL, + "hp": 16, + "attack": 6, + "defense": 2 + } +} + +# ============================================================================= +# Difficulty Scaling with Equipment +# ============================================================================= + +def get_max_enemies_per_room(level: int) -> int: + return min(2 + level, 6) + +def get_max_items_per_room(level: int) -> int: + return min(1 + level // 2, 4) + +def get_enemy_weights(level: int) -> list[tuple[str, float]]: + if level <= 2: + return [("goblin", 0.8), ("orc", 0.95), ("troll", 1.0)] + elif level <= 4: + return [("goblin", 0.5), ("orc", 0.85), ("troll", 1.0)] + else: + return [("goblin", 0.3), ("orc", 0.6), ("troll", 1.0)] + +def get_item_weights(level: int) -> list[tuple[str, float]]: + """Get item spawn weights based on dungeon level.""" + if level <= 1: + return [ + ("health_potion", 0.7), + ("dagger", 0.85), + ("leather_armor", 1.0) + ] + elif level <= 3: + return [ + ("health_potion", 0.4), + ("greater_health_potion", 0.55), + ("dagger", 0.65), + ("sword", 0.75), + ("leather_armor", 0.85), + ("chain_mail", 1.0) + ] + else: + return [ + ("health_potion", 0.2), + ("greater_health_potion", 0.4), + ("sword", 0.55), + ("great_axe", 0.7), + ("chain_mail", 0.85), + ("plate_armor", 1.0) + ] + +# ============================================================================= +# Message Log System +# ============================================================================= + +class MessageLog: + def __init__(self, x: int, y: int, width: int, height: int, max_messages: int = 6): + self.x = x + self.y = y + self.width = width + self.height = height + self.max_messages = max_messages + self.messages: list[tuple[str, mcrfpy.Color]] = [] + self.captions: list[mcrfpy.Caption] = [] + + self.frame = mcrfpy.Frame(pos=(x, y), size=(width, height)) + self.frame.fill_color = mcrfpy.Color(20, 20, 30, 200) + self.frame.outline = 2 + self.frame.outline_color = mcrfpy.Color(80, 80, 100) + + line_height = 20 + for i in range(max_messages): + caption = mcrfpy.Caption(pos=(x + 10, y + 5 + i * line_height), text="") + caption.font_size = 14 + caption.fill_color = mcrfpy.Color(200, 200, 200) + self.captions.append(caption) + + def add_to_scene(self, scene: mcrfpy.Scene) -> None: + scene.children.append(self.frame) + for caption in self.captions: + scene.children.append(caption) + + def add(self, text: str, color: mcrfpy.Color = None) -> None: + if color is None: + color = mcrfpy.Color(200, 200, 200) + self.messages.append((text, color)) + while len(self.messages) > self.max_messages: + self.messages.pop(0) + self._refresh() + + def _refresh(self) -> None: + for i, caption in enumerate(self.captions): + if i < len(self.messages): + text, color = self.messages[i] + caption.text = text + caption.fill_color = color + else: + caption.text = "" + + def clear(self) -> None: + self.messages.clear() + self._refresh() + +# ============================================================================= +# Health Bar System +# ============================================================================= + +class HealthBar: + def __init__(self, x: int, y: int, width: int, height: int): + self.x, self.y = x, y + self.width, self.height = width, height + + self.bg_frame = mcrfpy.Frame(pos=(x, y), size=(width, height)) + self.bg_frame.fill_color = mcrfpy.Color(80, 0, 0) + self.bg_frame.outline = 2 + self.bg_frame.outline_color = mcrfpy.Color(150, 150, 150) + + self.fg_frame = mcrfpy.Frame(pos=(x + 2, y + 2), size=(width - 4, height - 4)) + self.fg_frame.fill_color = mcrfpy.Color(0, 180, 0) + self.fg_frame.outline = 0 + + self.label = mcrfpy.Caption(pos=(x + 5, y + 2), text="HP: 30/30") + self.label.font_size = 16 + self.label.fill_color = mcrfpy.Color(255, 255, 255) + + def add_to_scene(self, scene: mcrfpy.Scene) -> None: + scene.children.append(self.bg_frame) + scene.children.append(self.fg_frame) + scene.children.append(self.label) + + def update(self, current_hp: int, max_hp: int) -> None: + percent = max(0, current_hp / max_hp) if max_hp > 0 else 0 + self.fg_frame.resize(int((self.width - 4) * percent), self.height - 4) + self.label.text = f"HP: {current_hp}/{max_hp}" + if percent > 0.6: + self.fg_frame.fill_color = mcrfpy.Color(0, 180, 0) + elif percent > 0.3: + self.fg_frame.fill_color = mcrfpy.Color(180, 180, 0) + else: + self.fg_frame.fill_color = mcrfpy.Color(180, 0, 0) + +# ============================================================================= +# XP Bar System +# ============================================================================= + +class XPBar: + def __init__(self, x: int, y: int, width: int, height: int): + self.x, self.y = x, y + self.width, self.height = width, height + + self.bg_frame = mcrfpy.Frame(pos=(x, y), size=(width, height)) + self.bg_frame.fill_color = mcrfpy.Color(40, 40, 80) + self.bg_frame.outline = 2 + self.bg_frame.outline_color = mcrfpy.Color(100, 100, 150) + + self.fg_frame = mcrfpy.Frame(pos=(x + 2, y + 2), size=(0, height - 4)) + self.fg_frame.fill_color = mcrfpy.Color(200, 200, 50) + self.fg_frame.outline = 0 + + self.label = mcrfpy.Caption(pos=(x + 5, y + 1), text="Level 1 | XP: 0/100") + self.label.font_size = 14 + self.label.fill_color = mcrfpy.Color(255, 255, 255) + + def add_to_scene(self, scene: mcrfpy.Scene) -> None: + scene.children.append(self.bg_frame) + scene.children.append(self.fg_frame) + scene.children.append(self.label) + + def update(self, level: int, xp: int, xp_to_next: int) -> None: + percent = min(1.0, max(0.0, xp / xp_to_next if xp_to_next > 0 else 0)) + self.fg_frame.resize(int((self.width - 4) * percent), self.height - 4) + self.label.text = f"Level {level} | XP: {xp}/{xp_to_next}" + +# ============================================================================= +# Equipment Panel +# ============================================================================= + +class EquipmentPanel: + """Panel showing equipped items.""" + + def __init__(self, x: int, y: int, width: int, height: int): + self.x, self.y = x, y + self.width, self.height = width, height + + self.frame = mcrfpy.Frame(pos=(x, y), size=(width, height)) + self.frame.fill_color = mcrfpy.Color(20, 20, 30, 200) + self.frame.outline = 2 + self.frame.outline_color = mcrfpy.Color(80, 80, 100) + + self.title = mcrfpy.Caption(pos=(x + 10, y + 5), text="Equipment") + self.title.font_size = 14 + self.title.fill_color = mcrfpy.Color(200, 200, 255) + + self.weapon_label = mcrfpy.Caption(pos=(x + 10, y + 25), text="Weapon: None") + self.weapon_label.font_size = 13 + self.weapon_label.fill_color = mcrfpy.Color(255, 150, 150) + + self.armor_label = mcrfpy.Caption(pos=(x + 10, y + 43), text="Armor: None") + self.armor_label.font_size = 13 + self.armor_label.fill_color = mcrfpy.Color(150, 150, 255) + + self.stats_label = mcrfpy.Caption(pos=(x + 10, y + 61), text="ATK: 5 | DEF: 2") + self.stats_label.font_size = 12 + self.stats_label.fill_color = mcrfpy.Color(200, 200, 200) + + def add_to_scene(self, scene: mcrfpy.Scene) -> None: + scene.children.append(self.frame) + scene.children.append(self.title) + scene.children.append(self.weapon_label) + scene.children.append(self.armor_label) + scene.children.append(self.stats_label) + + def update(self, fighter: "Fighter") -> None: + if fighter.weapon: + weapon_text = f"Weapon: {fighter.weapon.name} (+{fighter.weapon.attack_bonus} ATK)" + else: + weapon_text = "Weapon: None" + self.weapon_label.text = weapon_text + + if fighter.armor: + armor_text = f"Armor: {fighter.armor.name} (+{fighter.armor.defense_bonus} DEF)" + else: + armor_text = "Armor: None" + self.armor_label.text = armor_text + + self.stats_label.text = f"Total ATK: {fighter.attack} | DEF: {fighter.defense}" + +# ============================================================================= +# Inventory Panel +# ============================================================================= + +class InventoryPanel: + def __init__(self, x: int, y: int, width: int, height: int): + self.x, self.y = x, y + self.width, self.height = width, height + self.captions: list[mcrfpy.Caption] = [] + + self.frame = mcrfpy.Frame(pos=(x, y), size=(width, height)) + self.frame.fill_color = mcrfpy.Color(20, 20, 30, 200) + self.frame.outline = 2 + self.frame.outline_color = mcrfpy.Color(80, 80, 100) + + self.title = mcrfpy.Caption(pos=(x + 10, y + 5), text="Inventory (G:get E:equip)") + self.title.font_size = 14 + self.title.fill_color = mcrfpy.Color(200, 200, 255) + + for i in range(5): + caption = mcrfpy.Caption(pos=(x + 10, y + 25 + i * 18), text="") + caption.font_size = 13 + caption.fill_color = mcrfpy.Color(180, 180, 180) + self.captions.append(caption) + + def add_to_scene(self, scene: mcrfpy.Scene) -> None: + scene.children.append(self.frame) + scene.children.append(self.title) + for caption in self.captions: + scene.children.append(caption) + + def update(self, inventory: Inventory) -> None: + for i, caption in enumerate(self.captions): + if i < len(inventory.items): + item = inventory.items[i] + # Show item type indicator + if item.item_type == "equipment" and item.equipment: + if item.equipment.slot == "weapon": + caption.text = f"{i+1}. {item.name} [W]" + caption.fill_color = mcrfpy.Color(255, 150, 150) + else: + caption.text = f"{i+1}. {item.name} [A]" + caption.fill_color = mcrfpy.Color(150, 150, 255) + else: + caption.text = f"{i+1}. {item.name}" + caption.fill_color = mcrfpy.Color(180, 180, 180) + else: + caption.text = f"{i+1}. ---" + caption.fill_color = mcrfpy.Color(80, 80, 80) + +# ============================================================================= +# Level Display +# ============================================================================= + +class LevelDisplay: + def __init__(self, x: int, y: int): + self.caption = mcrfpy.Caption(pos=(x, y), text="Dungeon: 1") + self.caption.font_size = 16 + self.caption.fill_color = mcrfpy.Color(200, 200, 255) + + def add_to_scene(self, scene: mcrfpy.Scene) -> None: + scene.children.append(self.caption) + + def update(self, level: int) -> None: + self.caption.text = f"Dungeon: {level}" + +# ============================================================================= +# Mode Display +# ============================================================================= + +class ModeDisplay: + def __init__(self, x: int, y: int): + self.caption = mcrfpy.Caption(pos=(x, y), text="[NORMAL MODE]") + self.caption.font_size = 16 + self.caption.fill_color = mcrfpy.Color(100, 255, 100) + + def add_to_scene(self, scene: mcrfpy.Scene) -> None: + scene.children.append(self.caption) + + def update(self, mode: GameMode) -> None: + if mode == GameMode.NORMAL: + self.caption.text = "[NORMAL] F:Ranged | >:Descend | E:Equip" + self.caption.fill_color = mcrfpy.Color(100, 255, 100) + elif mode == GameMode.TARGETING: + self.caption.text = "[TARGETING] Arrows:Move, Enter:Fire, Esc:Cancel" + self.caption.fill_color = mcrfpy.Color(255, 255, 100) + +# ============================================================================= +# Global State +# ============================================================================= + +entity_data: dict[mcrfpy.Entity, Fighter] = {} +item_data: dict[mcrfpy.Entity, Item] = {} + +player: Optional[mcrfpy.Entity] = None +player_inventory: Optional[Inventory] = None +grid: Optional[mcrfpy.Grid] = None +fov_layer = None +texture: Optional[mcrfpy.Texture] = None +game_over: bool = False +dungeon_level: int = 1 +stairs_position: tuple[int, int] = (0, 0) + +game_mode: GameMode = GameMode.NORMAL +target_cursor: Optional[mcrfpy.Entity] = None +target_x: int = 0 +target_y: int = 0 + +# UI components +message_log: Optional[MessageLog] = None +health_bar: Optional[HealthBar] = None +xp_bar: Optional[XPBar] = None +inventory_panel: Optional[InventoryPanel] = None +equipment_panel: Optional[EquipmentPanel] = None +mode_display: Optional[ModeDisplay] = None +level_display: Optional[LevelDisplay] = None + +# ============================================================================= +# Room Class +# ============================================================================= + +class RectangularRoom: + def __init__(self, x: int, y: int, width: int, height: int): + self.x1, self.y1 = x, y + self.x2, self.y2 = x + width, y + height + + @property + def center(self) -> tuple[int, int]: + return (self.x1 + self.x2) // 2, (self.y1 + self.y2) // 2 + + @property + def inner(self) -> tuple[slice, slice]: + return slice(self.x1 + 1, self.x2), slice(self.y1 + 1, self.y2) + + def intersects(self, other: "RectangularRoom") -> bool: + return self.x1 <= other.x2 and self.x2 >= other.x1 and self.y1 <= other.y2 and self.y2 >= other.y1 + +# ============================================================================= +# Exploration Tracking +# ============================================================================= + +explored: list[list[bool]] = [] + +def init_explored() -> None: + global explored + explored = [[False for _ in range(GRID_WIDTH)] for _ in range(GRID_HEIGHT)] + +def mark_explored(x: int, y: int) -> None: + if 0 <= x < GRID_WIDTH and 0 <= y < GRID_HEIGHT: + explored[y][x] = True + +def is_explored(x: int, y: int) -> bool: + if 0 <= x < GRID_WIDTH and 0 <= y < GRID_HEIGHT: + return explored[y][x] + return False + +# ============================================================================= +# Dungeon Generation (abbreviated for space) +# ============================================================================= + +def fill_with_walls(target_grid: mcrfpy.Grid) -> None: + for y in range(GRID_HEIGHT): + for x in range(GRID_WIDTH): + cell = target_grid.at(x, y) + cell.tilesprite = SPRITE_WALL + cell.walkable = False + cell.transparent = False + +def carve_room(target_grid: mcrfpy.Grid, room: RectangularRoom) -> None: + inner_x, inner_y = room.inner + for y in range(inner_y.start, inner_y.stop): + for x in range(inner_x.start, inner_x.stop): + if 0 <= x < GRID_WIDTH and 0 <= y < GRID_HEIGHT: + cell = target_grid.at(x, y) + cell.tilesprite = SPRITE_FLOOR + cell.walkable = True + cell.transparent = True + +def carve_tunnel_horizontal(target_grid: mcrfpy.Grid, x1: int, x2: int, y: int) -> None: + for x in range(min(x1, x2), max(x1, x2) + 1): + if 0 <= x < GRID_WIDTH and 0 <= y < GRID_HEIGHT: + cell = target_grid.at(x, y) + cell.tilesprite = SPRITE_FLOOR + cell.walkable = True + cell.transparent = True + +def carve_tunnel_vertical(target_grid: mcrfpy.Grid, y1: int, y2: int, x: int) -> None: + for y in range(min(y1, y2), max(y1, y2) + 1): + if 0 <= x < GRID_WIDTH and 0 <= y < GRID_HEIGHT: + cell = target_grid.at(x, y) + cell.tilesprite = SPRITE_FLOOR + cell.walkable = True + cell.transparent = True + +def carve_l_tunnel(target_grid: mcrfpy.Grid, start: tuple[int, int], end: tuple[int, int]) -> None: + x1, y1 = start + x2, y2 = end + if random.random() < 0.5: + carve_tunnel_horizontal(target_grid, x1, x2, y1) + carve_tunnel_vertical(target_grid, y1, y2, x2) + else: + carve_tunnel_vertical(target_grid, y1, y2, x1) + carve_tunnel_horizontal(target_grid, x1, x2, y2) + +def place_stairs(target_grid: mcrfpy.Grid, x: int, y: int) -> None: + global stairs_position + cell = target_grid.at(x, y) + cell.tilesprite = SPRITE_STAIRS_DOWN + cell.walkable = True + cell.transparent = True + stairs_position = (x, y) + +def generate_dungeon(target_grid: mcrfpy.Grid, level: int) -> tuple[int, int]: + fill_with_walls(target_grid) + rooms: list[RectangularRoom] = [] + + for _ in range(MAX_ROOMS): + room_width = random.randint(ROOM_MIN_SIZE, ROOM_MAX_SIZE) + room_height = random.randint(ROOM_MIN_SIZE, ROOM_MAX_SIZE) + x = random.randint(1, GRID_WIDTH - room_width - 2) + y = random.randint(1, GRID_HEIGHT - room_height - 2) + + new_room = RectangularRoom(x, y, room_width, room_height) + + if any(new_room.intersects(other) for other in rooms): + continue + + carve_room(target_grid, new_room) + if rooms: + carve_l_tunnel(target_grid, new_room.center, rooms[-1].center) + rooms.append(new_room) + + if rooms: + place_stairs(target_grid, *rooms[-1].center) + return rooms[0].center + return GRID_WIDTH // 2, GRID_HEIGHT // 2 + +# ============================================================================= +# Entity Management +# ============================================================================= + +def spawn_enemy(target_grid: mcrfpy.Grid, x: int, y: int, enemy_type: str, tex: mcrfpy.Texture) -> mcrfpy.Entity: + template = ENEMY_TEMPLATES[enemy_type] + enemy = mcrfpy.Entity(grid_pos=(x, y), texture=tex, sprite_index=template["sprite"]) + enemy.visible = False + target_grid.entities.append(enemy) + entity_data[enemy] = Fighter( + hp=template["hp"], max_hp=template["hp"], + base_attack=template["attack"], base_defense=template["defense"], + name=enemy_type.capitalize(), is_player=False + ) + return enemy + +def spawn_item(target_grid: mcrfpy.Grid, x: int, y: int, item_type: str, tex: mcrfpy.Texture) -> mcrfpy.Entity: + template = ITEM_TEMPLATES[item_type] + item_entity = mcrfpy.Entity(grid_pos=(x, y), texture=tex, sprite_index=template["sprite"]) + item_entity.visible = False + target_grid.entities.append(item_entity) + + # Create the item + item = Item( + name=template["name"], + item_type=template["item_type"], + heal_amount=template.get("heal_amount", 0) + ) + + # If it is equipment, create the equipment data + if template["item_type"] == "equipment": + item.equipment = Equipment( + name=template["name"], + slot=template["slot"], + attack_bonus=template.get("attack_bonus", 0), + defense_bonus=template.get("defense_bonus", 0) + ) + + item_data[item_entity] = item + return item_entity + +def spawn_entities_for_level(target_grid: mcrfpy.Grid, tex: mcrfpy.Texture, level: int) -> None: + floor_tiles = [] + for y in range(GRID_HEIGHT): + for x in range(GRID_WIDTH): + cell = target_grid.at(x, y) + if cell.walkable and cell.tilesprite == SPRITE_FLOOR: + floor_tiles.append((x, y)) + + # Spawn enemies + max_enemies = get_max_enemies_per_room(level) * 3 + enemy_weights = get_enemy_weights(level) + + for _ in range(max_enemies): + if not floor_tiles: + break + x, y = random.choice(floor_tiles) + if (x, y) == (player.grid_x, player.grid_y) or (x, y) == stairs_position: + continue + if is_position_occupied(target_grid, x, y): + continue + + roll = random.random() + enemy_type = "goblin" + for etype, threshold in enemy_weights: + if roll < threshold: + enemy_type = etype + break + spawn_enemy(target_grid, x, y, enemy_type, tex) + + # Spawn items + max_items = get_max_items_per_room(level) * 2 + item_weights = get_item_weights(level) + + for _ in range(max_items): + if not floor_tiles: + break + x, y = random.choice(floor_tiles) + if (x, y) == (player.grid_x, player.grid_y) or (x, y) == stairs_position: + continue + if is_position_occupied(target_grid, x, y): + continue + + roll = random.random() + item_type = "health_potion" + for itype, threshold in item_weights: + if roll < threshold: + item_type = itype + break + spawn_item(target_grid, x, y, item_type, tex) + +def is_position_occupied(target_grid: mcrfpy.Grid, x: int, y: int) -> bool: + for entity in target_grid.entities: + if entity.grid_x == x and entity.grid_y == y: + return True + return False + +def get_item_at(target_grid: mcrfpy.Grid, x: int, y: int) -> Optional[mcrfpy.Entity]: + for entity in target_grid.entities: + if entity in item_data and entity.grid_x == x and entity.grid_y == y: + return entity + return None + +def get_blocking_entity_at(target_grid: mcrfpy.Grid, x: int, y: int, exclude: mcrfpy.Entity = None) -> Optional[mcrfpy.Entity]: + for entity in target_grid.entities: + if entity == exclude: + continue + if entity.grid_x == x and entity.grid_y == y: + if entity in entity_data and entity_data[entity].is_alive: + return entity + return None + +def remove_entity(target_grid: mcrfpy.Grid, entity: mcrfpy.Entity) -> None: + for e in target_grid.entities: + if e == entity: + target_grid.entities.remove(e) + break + if entity in entity_data: + del entity_data[entity] + +def remove_item_entity(target_grid: mcrfpy.Grid, entity: mcrfpy.Entity) -> None: + for e in target_grid.entities: + if e == entity: + target_grid.entities.remove(e) + break + if entity in item_data: + del item_data[entity] + +def clear_entities_except_player(target_grid: mcrfpy.Grid) -> None: + entities_to_remove = [] + for entity in target_grid.entities: + if entity in entity_data and entity_data[entity].is_player: + continue + entities_to_remove.append(entity) + + for entity in entities_to_remove: + if entity in entity_data: + del entity_data[entity] + if entity in item_data: + del item_data[entity] + for e in target_grid.entities: + if e == entity: + target_grid.entities.remove(e) + break + +# ============================================================================= +# Equipment Actions +# ============================================================================= + +def equip_item(index: int) -> bool: + """Equip an item from inventory. + + Args: + index: Inventory index of item to equip + + Returns: + True if item was equipped, False otherwise + """ + global player, player_inventory + + item = player_inventory.get(index) + if item is None: + message_log.add("Invalid item selection.", COLOR_INVALID) + return False + + if item.item_type != "equipment" or item.equipment is None: + message_log.add(f"The {item.name} cannot be equipped.", COLOR_INVALID) + return False + + fighter = entity_data.get(player) + if fighter is None: + return False + + # Remove from inventory + player_inventory.remove(index) + + # Equip and get old equipment + old_equipment = fighter.equip(item.equipment) + + message_log.add(f"You equip the {item.name}.", COLOR_EQUIP) + + # Add old equipment back to inventory + if old_equipment: + old_item = Item( + name=old_equipment.name, + item_type="equipment", + equipment=old_equipment + ) + if player_inventory.add(old_item): + message_log.add(f"You unequip the {old_equipment.name}.", COLOR_INFO) + else: + # Inventory full - drop on ground + drop_equipment(old_equipment) + message_log.add(f"Inventory full! {old_equipment.name} dropped.", COLOR_WARNING) + + update_ui() + return True + +def drop_equipment(equipment: Equipment) -> None: + """Drop equipment on the ground at player position.""" + global player, grid, texture + + px, py = player.grid_x, player.grid_y + + # Find template for this equipment + template = None + for key, tmpl in ITEM_TEMPLATES.items(): + if tmpl["name"] == equipment.name: + template = tmpl + break + + if template is None: + # Use default appearance + template = { + "sprite": SPRITE_SWORD if equipment.slot == "weapon" else SPRITE_ARMOR + } + + item_entity = mcrfpy.Entity( + grid_pos=(px, py), + texture=texture, + sprite_index=template["sprite"] + ) + item_entity.visible = True + + grid.entities.append(item_entity) + + item_data[item_entity] = Item( + name=equipment.name, + item_type="equipment", + equipment=equipment + ) + +# ============================================================================= +# XP and Level Up +# ============================================================================= + +def award_xp(enemy_name: str) -> None: + global player + fighter = entity_data.get(player) + if fighter is None: + return + + xp_amount = ENEMY_XP_VALUES.get(enemy_name.lower(), 35) + leveled_up = fighter.gain_xp(xp_amount) + + if leveled_up: + message_log.add( + f"You gained {xp_amount} XP and reached level {fighter.level}!", + COLOR_LEVEL_UP + ) + message_log.add( + f"HP +5, Attack +1{', Defense +1' if fighter.level % 3 == 0 else ''}!", + COLOR_LEVEL_UP + ) + else: + message_log.add(f"You gain {xp_amount} XP.", COLOR_XP) + update_ui() + +# ============================================================================= +# Level Transition +# ============================================================================= + +def descend_stairs() -> bool: + global player, dungeon_level, grid, fov_layer, stairs_position + + px, py = player.grid_x, player.grid_y + if (px, py) != stairs_position: + message_log.add("There are no stairs here.", COLOR_INVALID) + return False + + dungeon_level += 1 + clear_entities_except_player(grid) + init_explored() + player_start = generate_dungeon(grid, dungeon_level) + player.grid_pos = player_start + spawn_entities_for_level(grid, texture, dungeon_level) + + for y in range(GRID_HEIGHT): + for x in range(GRID_WIDTH): + fov_layer.set((x, y), COLOR_UNKNOWN) + update_fov(grid, fov_layer, player_start[0], player_start[1]) + + message_log.add(f"You descend to level {dungeon_level}...", COLOR_DESCEND) + level_display.update(dungeon_level) + update_ui() + return True + +# ============================================================================= +# Save/Load System +# ============================================================================= + +def save_game() -> bool: + global player, player_inventory, grid, explored, dungeon_level, stairs_position + + try: + tiles = [] + for y in range(GRID_HEIGHT): + row = [] + for x in range(GRID_WIDTH): + cell = grid.at(x, y) + row.append({ + "tilesprite": cell.tilesprite, + "walkable": cell.walkable, + "transparent": cell.transparent + }) + tiles.append(row) + + enemies = [] + for entity in grid.entities: + if entity == player: + continue + if entity in entity_data: + enemies.append({ + "x": entity.grid_x, "y": entity.grid_y, + "type": entity_data[entity].name.lower(), + "fighter": entity_data[entity].to_dict() + }) + + items_on_ground = [] + for entity in grid.entities: + if entity in item_data: + items_on_ground.append({ + "x": entity.grid_x, "y": entity.grid_y, + "item": item_data[entity].to_dict() + }) + + save_data = { + "version": 4, + "dungeon_level": dungeon_level, + "stairs_position": list(stairs_position), + "player": { + "x": player.grid_x, "y": player.grid_y, + "fighter": entity_data[player].to_dict(), + "inventory": player_inventory.to_dict() + }, + "tiles": tiles, + "explored": [[explored[y][x] for x in range(GRID_WIDTH)] for y in range(GRID_HEIGHT)], + "enemies": enemies, + "items": items_on_ground + } + + with open(SAVE_FILE, "w") as f: + json.dump(save_data, f, indent=2) + + message_log.add("Game saved!", COLOR_SAVE) + return True + except Exception as e: + message_log.add(f"Save failed: {e}", COLOR_INVALID) + return False + +def load_game() -> bool: + global player, player_inventory, grid, explored, dungeon_level + global entity_data, item_data, fov_layer, game_over, stairs_position + + if not os.path.exists(SAVE_FILE): + return False + + try: + with open(SAVE_FILE, "r") as f: + save_data = json.load(f) + + entity_data.clear() + item_data.clear() + while len(grid.entities) > 0: + grid.entities.remove(grid.entities[0]) + + dungeon_level = save_data.get("dungeon_level", 1) + stairs_position = tuple(save_data.get("stairs_position", [0, 0])) + + tiles = save_data["tiles"] + for y in range(GRID_HEIGHT): + for x in range(GRID_WIDTH): + cell = grid.at(x, y) + tile_data = tiles[y][x] + cell.tilesprite = tile_data["tilesprite"] + cell.walkable = tile_data["walkable"] + cell.transparent = tile_data["transparent"] + + global explored + explored = save_data["explored"] + + player_data = save_data["player"] + player = mcrfpy.Entity( + grid_pos=(player_data["x"], player_data["y"]), + texture=texture, sprite_index=SPRITE_PLAYER + ) + grid.entities.append(player) + entity_data[player] = Fighter.from_dict(player_data["fighter"]) + player_inventory = Inventory.from_dict(player_data["inventory"]) + + for enemy_data in save_data.get("enemies", []): + template = ENEMY_TEMPLATES.get(enemy_data["type"], ENEMY_TEMPLATES["goblin"]) + enemy = mcrfpy.Entity( + grid_pos=(enemy_data["x"], enemy_data["y"]), + texture=texture, sprite_index=template["sprite"] + ) + enemy.visible = False + grid.entities.append(enemy) + entity_data[enemy] = Fighter.from_dict(enemy_data["fighter"]) + + for item_entry in save_data.get("items", []): + item = Item.from_dict(item_entry["item"]) + # Find template for sprite + template = None + for key, tmpl in ITEM_TEMPLATES.items(): + if tmpl["name"] == item.name: + template = tmpl + break + if template is None: + template = ITEM_TEMPLATES["health_potion"] + + item_entity = mcrfpy.Entity( + grid_pos=(item_entry["x"], item_entry["y"]), + texture=texture, sprite_index=template["sprite"] + ) + item_entity.visible = False + grid.entities.append(item_entity) + item_data[item_entity] = item + + for y in range(GRID_HEIGHT): + for x in range(GRID_WIDTH): + fov_layer.set((x, y), COLOR_UNKNOWN) + update_fov(grid, fov_layer, player.grid_x, player.grid_y) + + game_over = False + message_log.add("Game loaded!", COLOR_SAVE) + level_display.update(dungeon_level) + update_ui() + return True + except Exception as e: + message_log.add(f"Load failed: {e}", COLOR_INVALID) + return False + +def delete_save() -> bool: + try: + if os.path.exists(SAVE_FILE): + os.remove(SAVE_FILE) + return True + except: + return False + +def has_save_file() -> bool: + return os.path.exists(SAVE_FILE) + +# ============================================================================= +# Targeting System +# ============================================================================= + +def enter_targeting_mode() -> None: + global game_mode, target_cursor, target_x, target_y + target_x, target_y = player.grid_x, player.grid_y + target_cursor = mcrfpy.Entity(grid_pos=(target_x, target_y), texture=texture, sprite_index=SPRITE_CURSOR) + grid.entities.append(target_cursor) + game_mode = GameMode.TARGETING + message_log.add("Targeting: Arrows to aim, Enter to fire, Esc to cancel.", COLOR_INFO) + mode_display.update(game_mode) + +def exit_targeting_mode() -> None: + global game_mode, target_cursor + if target_cursor: + for e in grid.entities: + if e == target_cursor: + grid.entities.remove(e) + break + target_cursor = None + game_mode = GameMode.NORMAL + mode_display.update(game_mode) + +def move_cursor(dx: int, dy: int) -> None: + global target_x, target_y + new_x, new_y = target_x + dx, target_y + dy + if not (0 <= new_x < GRID_WIDTH and 0 <= new_y < GRID_HEIGHT): + return + if not grid.is_in_fov(new_x, new_y): + message_log.add("Cannot see that location.", COLOR_INVALID) + return + distance = abs(new_x - player.grid_x) + abs(new_y - player.grid_y) + if distance > RANGED_ATTACK_RANGE: + message_log.add(f"Out of range (max {RANGED_ATTACK_RANGE}).", COLOR_WARNING) + return + target_x, target_y = new_x, new_y + target_cursor.grid_pos = (target_x, target_y) + enemy = get_blocking_entity_at(grid, target_x, target_y, exclude=player) + if enemy and enemy in entity_data: + f = entity_data[enemy] + message_log.add(f"Target: {f.name} (HP: {f.hp}/{f.max_hp})", COLOR_RANGED) + +def confirm_target() -> None: + if target_x == player.grid_x and target_y == player.grid_y: + message_log.add("Cannot target yourself!", COLOR_INVALID) + return + target_enemy = get_blocking_entity_at(grid, target_x, target_y, exclude=player) + if not target_enemy or target_enemy not in entity_data: + message_log.add("No valid target.", COLOR_INVALID) + return + perform_ranged_attack(target_enemy) + exit_targeting_mode() + enemy_turn() + update_ui() + +def perform_ranged_attack(target_entity: mcrfpy.Entity) -> None: + defender = entity_data.get(target_entity) + attacker = entity_data.get(player) + if not defender or not attacker: + return + damage = max(1, RANGED_ATTACK_DAMAGE - defender.defense // 2) + defender.take_damage(damage) + message_log.add(f"Ranged attack hits {defender.name} for {damage}!", COLOR_RANGED) + if not defender.is_alive: + handle_death(target_entity, defender) + +# ============================================================================= +# Combat System +# ============================================================================= + +def calculate_damage(attacker: Fighter, defender: Fighter) -> int: + return max(0, attacker.attack - defender.defense) + +def perform_attack(attacker_entity: mcrfpy.Entity, defender_entity: mcrfpy.Entity) -> None: + attacker = entity_data.get(attacker_entity) + defender = entity_data.get(defender_entity) + if not attacker or not defender: + return + + damage = calculate_damage(attacker, defender) + defender.take_damage(damage) + + if damage > 0: + if attacker.is_player: + message_log.add(f"You hit {defender.name} for {damage}!", COLOR_PLAYER_ATTACK) + else: + message_log.add(f"{attacker.name} hits you for {damage}!", COLOR_ENEMY_ATTACK) + else: + if attacker.is_player: + message_log.add(f"You hit {defender.name} but deal no damage.", mcrfpy.Color(150, 150, 150)) + else: + message_log.add(f"{attacker.name} hits but deals no damage.", mcrfpy.Color(150, 150, 200)) + + if not defender.is_alive: + handle_death(defender_entity, defender) + update_ui() + +def handle_death(entity: mcrfpy.Entity, fighter: Fighter) -> None: + global game_over + if fighter.is_player: + message_log.add("You have died!", COLOR_PLAYER_DEATH) + message_log.add("Press R to restart.", COLOR_INFO) + game_over = True + entity.sprite_index = SPRITE_CORPSE + delete_save() + else: + message_log.add(f"{fighter.name} dies!", COLOR_ENEMY_DEATH) + award_xp(fighter.name) + remove_entity(grid, entity) + +# ============================================================================= +# Item Actions +# ============================================================================= + +def pickup_item() -> bool: + px, py = player.grid_x, player.grid_y + item_entity = get_item_at(grid, px, py) + if not item_entity: + message_log.add("Nothing to pick up.", COLOR_INVALID) + return False + if player_inventory.is_full(): + message_log.add("Inventory full!", COLOR_WARNING) + return False + item = item_data.get(item_entity) + if not item: + return False + player_inventory.add(item) + remove_item_entity(grid, item_entity) + message_log.add(f"Picked up {item.name}.", COLOR_PICKUP) + update_ui() + return True + +def use_item(index: int) -> bool: + item = player_inventory.get(index) + if not item: + message_log.add("Invalid selection.", COLOR_INVALID) + return False + + # Handle equipment + if item.item_type == "equipment": + return equip_item(index) + + # Handle consumables + if item.item_type == "health_potion": + fighter = entity_data.get(player) + if not fighter: + return False + if fighter.hp >= fighter.max_hp: + message_log.add("Already at full health!", COLOR_WARNING) + return False + actual_heal = fighter.heal(item.heal_amount) + player_inventory.remove(index) + message_log.add(f"Healed {actual_heal} HP!", COLOR_HEAL) + update_ui() + return True + + message_log.add(f"Cannot use {item.name}.", COLOR_INVALID) + return False + +# ============================================================================= +# Field of View +# ============================================================================= + +def update_entity_visibility(target_grid: mcrfpy.Grid) -> None: + for entity in target_grid.entities: + if entity == player or entity == target_cursor: + entity.visible = True + else: + entity.visible = target_grid.is_in_fov(entity.grid_x, entity.grid_y) + +def update_fov(target_grid: mcrfpy.Grid, target_fov_layer, player_x: int, player_y: int) -> None: + target_grid.compute_fov((player_x, player_y), FOV_RADIUS, True, mcrfpy.FOV.SHADOW) + for y in range(GRID_HEIGHT): + for x in range(GRID_WIDTH): + if target_grid.is_in_fov(x, y): + mark_explored(x, y) + target_fov_layer.set((x, y), COLOR_VISIBLE) + elif is_explored(x, y): + target_fov_layer.set((x, y), COLOR_DISCOVERED) + else: + target_fov_layer.set((x, y), COLOR_UNKNOWN) + update_entity_visibility(target_grid) + +# ============================================================================= +# Movement +# ============================================================================= + +def can_move_to(target_grid: mcrfpy.Grid, x: int, y: int, mover: mcrfpy.Entity = None) -> bool: + if not (0 <= x < GRID_WIDTH and 0 <= y < GRID_HEIGHT): + return False + if not target_grid.at(x, y).walkable: + return False + return get_blocking_entity_at(target_grid, x, y, exclude=mover) is None + +def try_move_or_attack(dx: int, dy: int) -> None: + global game_over + if game_over: + return + px, py = player.grid_x, player.grid_y + new_x, new_y = px + dx, py + dy + if not (0 <= new_x < GRID_WIDTH and 0 <= new_y < GRID_HEIGHT): + return + + blocker = get_blocking_entity_at(grid, new_x, new_y, exclude=player) + if blocker: + perform_attack(player, blocker) + enemy_turn() + elif grid.at(new_x, new_y).walkable: + player.grid_pos = (new_x, new_y) + update_fov(grid, fov_layer, new_x, new_y) + enemy_turn() + update_ui() + +# ============================================================================= +# Enemy AI +# ============================================================================= + +def enemy_turn() -> None: + global game_over + if game_over: + return + px, py = player.grid_x, player.grid_y + + for entity in list(grid.entities): + if entity == player or entity not in entity_data: + continue + fighter = entity_data[entity] + if not fighter.is_alive: + continue + ex, ey = entity.grid_x, entity.grid_y + if not grid.is_in_fov(ex, ey): + continue + + dx, dy = px - ex, py - ey + if abs(dx) <= 1 and abs(dy) <= 1 and (dx or dy): + perform_attack(entity, player) + else: + move_toward_player(entity, ex, ey, px, py) + +def move_toward_player(enemy: mcrfpy.Entity, ex: int, ey: int, px: int, py: int) -> None: + dx = 1 if px > ex else (-1 if px < ex else 0) + dy = 1 if py > ey else (-1 if py < ey else 0) + + if can_move_to(grid, ex + dx, ey + dy, enemy): + enemy.grid_pos = (ex + dx, ey + dy) + elif dx and can_move_to(grid, ex + dx, ey, enemy): + enemy.grid_x = ex + dx + elif dy and can_move_to(grid, ex, ey + dy, enemy): + enemy.grid_y = ey + dy + +# ============================================================================= +# UI Updates +# ============================================================================= + +def update_ui() -> None: + if player in entity_data: + fighter = entity_data[player] + health_bar.update(fighter.hp, fighter.max_hp) + xp_bar.update(fighter.level, fighter.xp, fighter.xp_to_next_level) + equipment_panel.update(fighter) + if player_inventory: + inventory_panel.update(player_inventory) + +# ============================================================================= +# New Game +# ============================================================================= + +def generate_new_game() -> None: + global player, player_inventory, game_over, dungeon_level, game_mode + + game_over = False + game_mode = GameMode.NORMAL + dungeon_level = 1 + + entity_data.clear() + item_data.clear() + while len(grid.entities) > 0: + grid.entities.remove(grid.entities[0]) + + init_explored() + message_log.clear() + + player_start = generate_dungeon(grid, dungeon_level) + + player = mcrfpy.Entity(grid_pos=player_start, texture=texture, sprite_index=SPRITE_PLAYER) + grid.entities.append(player) + + entity_data[player] = Fighter( + hp=30, max_hp=30, base_attack=5, base_defense=2, + name="Player", is_player=True, xp=0, level=1 + ) + + player_inventory = Inventory(capacity=10) + spawn_entities_for_level(grid, texture, dungeon_level) + + for y in range(GRID_HEIGHT): + for x in range(GRID_WIDTH): + fov_layer.set((x, y), COLOR_UNKNOWN) + update_fov(grid, fov_layer, player_start[0], player_start[1]) + + mode_display.update(game_mode) + level_display.update(dungeon_level) + update_ui() + +# ============================================================================= +# Game Setup +# ============================================================================= + +scene = mcrfpy.Scene("game") +texture = mcrfpy.Texture("assets/kenney_tinydungeon.png", 16, 16) + +grid = mcrfpy.Grid( + pos=(20, GAME_AREA_Y), + size=(700, GAME_AREA_HEIGHT - 20), + grid_size=(GRID_WIDTH, GRID_HEIGHT), + texture=texture, + zoom=1.0 +) + +fov_layer = mcrfpy.ColorLayer(z_index=-1, name="fov") +grid.add_layer(fov_layer) +for y in range(GRID_HEIGHT): + for x in range(GRID_WIDTH): + fov_layer.set((x, y), COLOR_UNKNOWN) + +scene.children.append(grid) + +# UI Elements +title = mcrfpy.Caption(pos=(20, 10), text="Part 13: Equipment System") +title.fill_color = mcrfpy.Color(255, 255, 255) +title.font_size = 24 +scene.children.append(title) + +instructions = mcrfpy.Caption(pos=(300, 15), text="WASD:Move | E:Equip | G:Get | >:Descend") +instructions.fill_color = mcrfpy.Color(180, 180, 180) +instructions.font_size = 14 +scene.children.append(instructions) + +health_bar = HealthBar(x=730, y=10, width=280, height=25) +health_bar.add_to_scene(scene) + +xp_bar = XPBar(x=730, y=40, width=280, height=20) +xp_bar.add_to_scene(scene) + +level_display = LevelDisplay(x=860, y=65) +level_display.add_to_scene(scene) + +mode_display = ModeDisplay(x=20, y=40) +mode_display.add_to_scene(scene) + +equipment_panel = EquipmentPanel(x=730, y=GAME_AREA_Y, width=280, height=85) +equipment_panel.add_to_scene(scene) + +inventory_panel = InventoryPanel(x=730, y=GAME_AREA_Y + 90, width=280, height=120) +inventory_panel.add_to_scene(scene) + +message_log = MessageLog(x=20, y=768 - UI_BOTTOM_HEIGHT + 10, width=990, height=UI_BOTTOM_HEIGHT - 20, max_messages=6) +message_log.add_to_scene(scene) + +# Initialize +init_explored() + +if has_save_file(): + message_log.add("Loading saved game...", COLOR_INFO) + if not load_game(): + generate_new_game() + message_log.add("Welcome to the dungeon!", COLOR_INFO) +else: + generate_new_game() + message_log.add("Welcome to the dungeon!", COLOR_INFO) + message_log.add("Find equipment to grow stronger!", COLOR_INFO) + +# ============================================================================= +# Input Handling +# ============================================================================= + +def handle_keys(key: mcrfpy.Key, action: mcrfpy.InputState) -> None: + global game_over, game_mode + + if action != mcrfpy.InputState.PRESSED: + return + + if key == mcrfpy.Key.R: + delete_save() + generate_new_game() + message_log.add("New adventure begins!", COLOR_INFO) + return + + if key == mcrfpy.Key.ESCAPE: + if game_mode == GameMode.TARGETING: + exit_targeting_mode() + message_log.add("Targeting cancelled.", COLOR_INFO) + else: + if not game_over: + save_game() + mcrfpy.exit() + return + + if game_over: + return + + if game_mode == GameMode.TARGETING: + handle_targeting_input(key) + else: + handle_normal_input(key) + +EQUIP_SLOT_KEYS = { + mcrfpy.Key.NUM_1: 0, + mcrfpy.Key.NUM_2: 1, + mcrfpy.Key.NUM_3: 2, + mcrfpy.Key.NUM_4: 3, + mcrfpy.Key.NUM_5: 4, +} + +def handle_normal_input(key: mcrfpy.Key) -> None: + if key in (mcrfpy.Key.W, mcrfpy.Key.UP): + try_move_or_attack(0, -1) + elif key in (mcrfpy.Key.S, mcrfpy.Key.DOWN): + try_move_or_attack(0, 1) + elif key in (mcrfpy.Key.A, mcrfpy.Key.LEFT): + try_move_or_attack(-1, 0) + elif key in (mcrfpy.Key.D, mcrfpy.Key.RIGHT): + try_move_or_attack(1, 0) + elif key == mcrfpy.Key.F: + enter_targeting_mode() + elif key in (mcrfpy.Key.G, mcrfpy.Key.COMMA): + pickup_item() + elif key == mcrfpy.Key.PERIOD: + descend_stairs() + elif key == mcrfpy.Key.E: + message_log.add("Press 1-5 to equip an item from inventory.", COLOR_INFO) + elif key in EQUIP_SLOT_KEYS: + index = EQUIP_SLOT_KEYS[key] + if use_item(index): + enemy_turn() + update_ui() + +def handle_targeting_input(key: mcrfpy.Key) -> None: + if key in (mcrfpy.Key.UP, mcrfpy.Key.W): + move_cursor(0, -1) + elif key in (mcrfpy.Key.DOWN, mcrfpy.Key.S): + move_cursor(0, 1) + elif key in (mcrfpy.Key.LEFT, mcrfpy.Key.A): + move_cursor(-1, 0) + elif key in (mcrfpy.Key.RIGHT, mcrfpy.Key.D): + move_cursor(1, 0) + elif key in (mcrfpy.Key.ENTER, mcrfpy.Key.SPACE): + confirm_target() + +scene.on_key = handle_keys + +# ============================================================================= +# Start the Game +# ============================================================================= + +scene.activate() +print("Part 13: Equipment System - Tutorial Complete!") +print("Find weapons and armor to become stronger!") diff --git a/tools/wf_docs_rewrite.js b/tools/wf_docs_rewrite.js new file mode 100644 index 0000000..a1157b1 --- /dev/null +++ b/tools/wf_docs_rewrite.js @@ -0,0 +1,256 @@ +export const meta = { + name: 'docs-rewrite-review', + description: 'Fix + review every hand-written docs page against the live engine API; stage runnable snippets', + phases: [ + { title: 'Fix', detail: 'Sonnet: correct code blocks against ground truth, stage runnable snippets' }, + { title: 'Review', detail: 'Opus: verify against the compiled API, catch hallucinated APIs, set disposition' }, + { title: 'Finalize', detail: 'Sonnet: apply review patches, stamp frontmatter' }, + ], +} + +const A = typeof args === 'string' ? JSON.parse(args) : args +const ENGINE = A.engineRoot +const SITE = A.siteRoot +const CHEAT = A.cheatsheet +const VERSION = A.version +const COMMIT = A.commit + +const FIX_SCHEMA = { + type: 'object', + additionalProperties: false, + required: ['page', 'code_blocks_total', 'code_blocks_changed', 'defects', 'snippets_staged', 'notes'], + properties: { + page: { type: 'string' }, + code_blocks_total: { type: 'integer' }, + code_blocks_changed: { type: 'integer' }, + defects: { + type: 'array', + items: { + type: 'object', additionalProperties: false, + required: ['location', 'problem', 'fix'], + properties: { + location: { type: 'string' }, + problem: { type: 'string' }, + fix: { type: 'string' }, + }, + }, + }, + snippets_staged: { + type: 'array', + items: { + type: 'object', additionalProperties: false, + required: ['file', 'ran', 'detail'], + properties: { + file: { type: 'string' }, + ran: { type: 'string', enum: ['pass', 'fail', 'not-run'] }, + detail: { type: 'string' }, + }, + }, + }, + notes: { type: 'string' }, + }, +} + +const REVIEW_SCHEMA = { + type: 'object', + additionalProperties: false, + required: ['page', 'verdict', 'api_errors_remaining', 'prose_issues', 'required_patches', 'final_disposition', 'summary'], + properties: { + page: { type: 'string' }, + verdict: { type: 'string', enum: ['approved', 'needs_changes'] }, + api_errors_remaining: { + type: 'array', + items: { + type: 'object', additionalProperties: false, + required: ['location', 'problem', 'correct_form'], + properties: { + location: { type: 'string' }, + problem: { type: 'string' }, + correct_form: { type: 'string' }, + }, + }, + }, + prose_issues: { type: 'array', items: { type: 'string' } }, + required_patches: { + type: 'array', + items: { + type: 'object', additionalProperties: false, + required: ['location', 'change'], + properties: { + location: { type: 'string' }, + change: { type: 'string' }, + }, + }, + }, + final_disposition: { type: 'string', enum: ['current', 'needs-review', 'rewrite-pending'] }, + summary: { type: 'string' }, + }, +} + +const FINALIZE_SCHEMA = { + type: 'object', + additionalProperties: false, + required: ['page', 'patches_applied', 'frontmatter_stamped', 'final_disposition', 'notes'], + properties: { + page: { type: 'string' }, + patches_applied: { type: 'integer' }, + frontmatter_stamped: { type: 'boolean' }, + final_disposition: { type: 'string' }, + notes: { type: 'string' }, + }, +} + +function fixPrompt(page) { + return `You are correcting one McRogueFace documentation page so every code sample matches the +engine that ships today. This is mechanical correction work — accuracy over creativity. + +PAGE (edit it IN PLACE with Edit): ${SITE}/${page.path} +Its self-declared disposition is "${page.disposition}" — do NOT trust that grade; verify from scratch. + +GROUND TRUTH — read this file FIRST and treat it as authority: + ${CHEAT} +It lists the exact known-broken→correct API mappings and the 58 real exported classes. + +Also available to consult (grep/Read as needed): + - ${ENGINE}/docs/API_REFERENCE_DYNAMIC.md (authoritative signatures) + - ${ENGINE}/tests/snippets/*.py (130 proven-correct examples) + +DO: +1. Read the page. Find every fenced code block (\`\`\`python and \`\`\`). +2. For each block, check every mcrfpy usage against ground truth. Fix broken APIs in place + with Edit. If a block uses \`mcrfpy.Animation\`, \`add_layer("...")\`, \`sceneUI\`/\`setScene\`/ + \`createScene\`/\`keypressScene\`, \`GridPoint.color\`, \`compute_astar_path\`, \`entity.gridstate\`, + or any \`mcrfpy.X\` where X is not one of the 58 exports — it is WRONG; correct it. +3. Fix prose that describes the old/broken behavior so it matches the corrected code. +4. Do NOT touch the frontmatter — a later step stamps it. Do NOT set disposition. +5. \`tilesprite\` is legacy but NOT broken — leave it, optionally note the TileLayer form. + +STAGE RUNNABLE SNIPPETS: +For each code block that is (or can trivially be completed into) a COMPLETE self-contained +program per the cheatsheet's rules (imports mcrfpy, builds a Scene, sets current_scene, +appends a child, no sys.exit, no external files), extract it to: + ${ENGINE}/tests/snippets/_staging/${page.slug}__NN.py (NN = 01, 02, ...) +Then VALIDATE it (cwd ${ENGINE}): + LD_LIBRARY_PATH=build/lib build/mcrogueface --headless --exec tests/snippets/_staging/${page.slug}__NN.py --exec tests/snippets/_harness.py +Passes iff output has SNIPPET_OK and no Traceback. If it FAILS, fix the snippet and rerun +until it passes. If it cannot be made a complete runnable program, delete the staged file +and leave the code inline (corrected) — mark it not-run. Do not invent snippets for pages +that are pure prose/reference (many objects/*.md just document properties). + +Return the structured report. Be honest about what you could not verify.` +} + +function reviewPrompt(page, fix) { + return `You are the REVIEW gate for one McRogueFace documentation page that a first-pass agent just +corrected. Your job is to catch what it missed or got wrong — hallucinated APIs, signatures +that don't exist, prose that still contradicts the code, and to set the final disposition. +Be adversarial: assume the fix is incomplete until you've checked it against the compiled API. + +PAGE (already edited by the fix pass): ${SITE}/${page.path} + +GROUND TRUTH: + - ${CHEAT} (known-broken→correct, 58 real exports) + - ${ENGINE}/docs/API_REFERENCE_DYNAMIC.md (authoritative signatures — grep it to VERIFY + every method/property name and argument the page now uses actually exists) + - ${ENGINE}/tests/snippets/*.py (proven usage) + +The fix pass reported: +${JSON.stringify(fix, null, 2)} + +DO: +1. Re-read the page's code blocks. For every mcrfpy class, method, property, and keyword + argument, CONFIRM it exists with that signature in API_REFERENCE_DYNAMIC.md. Anything you + cannot confirm is an api_error_remaining with the correct_form. +2. Check prose still matches code. Check the staged snippets the fix pass claims PASS are + genuinely complete programs (you may re-run one if unsure, same command as the fix pass). +3. Decide final_disposition: + - "current": every code sample verified correct against the compiled API, prose matches. + - "needs-review": mostly right but a residual concern you can't resolve from the repo. + - "rewrite-pending": still materially broken. +4. If verdict is needs_changes, list required_patches CONCRETELY (location + exact change) so + a mechanical follow-up can apply them without judgement. Do not edit the page yourself. + +Return the structured verdict.` +} + +function finalizePrompt(page, review) { + return `Finalize one McRogueFace docs page. Mechanical only. + +PAGE (edit IN PLACE): ${SITE}/${page.path} + +The reviewer returned: +${JSON.stringify(review, null, 2)} + +DO: +1. If verdict was needs_changes, apply EACH required_patch exactly as described, using Edit. + Apply nothing the reviewer did not ask for. +2. Update the YAML frontmatter (between the first two \`---\` lines). Under the top-level + \`mcrf:\` key set/replace these three keys (keep all other frontmatter untouched): + disposition: ${review ? review.final_disposition : 'needs-review'} + verified: "${VERSION}" + verified_commit: ${COMMIT} + If a \`verified:\`/\`verified_commit:\` line already exists, replace its value; otherwise add + it directly under the existing \`disposition:\` line at the same indentation. +Return the structured report (patches_applied = count you applied; frontmatter_stamped true +once the three keys are set).` +} + +phase('Fix') +log(`Rewriting ${A.pages.length} docs pages: fix (Sonnet) -> review (Opus) -> finalize (Sonnet)`) + +const results = await pipeline( + A.pages, + (page) => agent(fixPrompt(page), { + label: `fix:${page.path}`, phase: 'Fix', model: 'sonnet', effort: 'medium', schema: FIX_SCHEMA, + }).then(fix => ({ page, fix })), + + (prev, page) => { + if (!prev || !prev.fix) return { page, fix: null, review: null } + return agent(reviewPrompt(page, prev.fix), { + label: `review:${page.path}`, phase: 'Review', model: 'opus', effort: 'high', schema: REVIEW_SCHEMA, + }).then(review => ({ page, fix: prev.fix, review })) + }, + + (prev, page) => { + const review = prev ? prev.review : null + return agent(finalizePrompt(page, review), { + label: `finalize:${page.path}`, phase: 'Finalize', model: 'sonnet', effort: 'low', schema: FINALIZE_SCHEMA, + }).then(fin => ({ + page: page.path, + disposition_before: page.disposition, + fix: prev ? prev.fix : null, + review, + finalize: fin, + })) + }, +) + +const clean = results.filter(Boolean) +const staged = [] +for (const r of clean) { + for (const s of (r.fix && r.fix.snippets_staged) || []) { + staged.push({ page: r.page, file: s.file, ran: s.ran, detail: s.detail }) + } +} +const byDisp = {} +for (const r of clean) { + const d = (r.finalize && r.finalize.final_disposition) || (r.review && r.review.final_disposition) || 'unknown' + byDisp[d] = (byDisp[d] || 0) + 1 +} +const needsChanges = clean.filter(r => r.review && r.review.verdict === 'needs_changes') + .map(r => ({ page: r.page, disposition: r.review.final_disposition, summary: r.review.summary, + api_errors: (r.review.api_errors_remaining || []).length })) +const stillBroken = clean.filter(r => (r.finalize && r.finalize.final_disposition) === 'rewrite-pending' + || (r.review && r.review.final_disposition === 'rewrite-pending')) + .map(r => r.page) + +log(`Done. dispositions: ${JSON.stringify(byDisp)}; staged snippets: ${staged.length}`) + +return { + pages_processed: clean.length, + pages_expected: A.pages.length, + disposition_counts: byDisp, + staged_snippets: staged, + pages_needing_changes: needsChanges, + still_rewrite_pending: stillBroken, +}