tilemap-editor 2.0.0__tar.gz → 2.0.2__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {tilemap_editor-2.0.0 → tilemap_editor-2.0.2}/CHANGELOG.md +19 -0
- {tilemap_editor-2.0.0/src/tilemap_editor.egg-info → tilemap_editor-2.0.2}/PKG-INFO +54 -5
- {tilemap_editor-2.0.0 → tilemap_editor-2.0.2}/README.md +53 -4
- {tilemap_editor-2.0.0 → tilemap_editor-2.0.2}/pyproject.toml +1 -1
- {tilemap_editor-2.0.0 → tilemap_editor-2.0.2}/src/editor.py +85 -0
- tilemap_editor-2.0.2/src/plugins/character_collision/__init__.py +24 -0
- tilemap_editor-2.0.2/src/plugins/character_collision/editor.py +320 -0
- tilemap_editor-2.0.2/src/plugins/character_collision/models.py +171 -0
- tilemap_editor-2.0.2/src/plugins/character_collision/protocols.py +169 -0
- tilemap_editor-2.0.2/src/plugins/character_collision/shape_editor.py +567 -0
- tilemap_editor-2.0.2/src/plugins/character_collision/standalone.py +125 -0
- tilemap_editor-2.0.2/src/plugins/tileset_collision/__init__.py +17 -0
- tilemap_editor-2.0.2/src/plugins/tileset_collision/collision_painter.py +759 -0
- tilemap_editor-2.0.2/src/plugins/tileset_collision/editor.py +702 -0
- tilemap_editor-2.0.2/src/plugins/tileset_collision/models.py +113 -0
- tilemap_editor-2.0.2/src/plugins/tileset_collision/protocols.py +56 -0
- tilemap_editor-2.0.2/src/plugins/tileset_collision/standalone.py +140 -0
- {tilemap_editor-2.0.0 → tilemap_editor-2.0.2/src/tilemap_editor.egg-info}/PKG-INFO +54 -5
- {tilemap_editor-2.0.0 → tilemap_editor-2.0.2}/src/tilemap_editor.egg-info/SOURCES.txt +12 -0
- {tilemap_editor-2.0.0 → tilemap_editor-2.0.2}/src/widgets/autotiler.py +22 -10
- {tilemap_editor-2.0.0 → tilemap_editor-2.0.2}/src/widgets/tile_selector.py +31 -1
- {tilemap_editor-2.0.0 → tilemap_editor-2.0.2}/src/widgets/ui/menubar.py +1 -0
- {tilemap_editor-2.0.0 → tilemap_editor-2.0.2}/LICENSE +0 -0
- {tilemap_editor-2.0.0 → tilemap_editor-2.0.2}/MANIFEST.in +0 -0
- {tilemap_editor-2.0.0 → tilemap_editor-2.0.2}/assets/fonts/jetbrain-fonts/JetBrainsMono-Bold.ttf +0 -0
- {tilemap_editor-2.0.0 → tilemap_editor-2.0.2}/assets/fonts/jetbrain-fonts/JetBrainsMono-BoldItalic.ttf +0 -0
- {tilemap_editor-2.0.0 → tilemap_editor-2.0.2}/assets/fonts/jetbrain-fonts/JetBrainsMono-Italic.ttf +0 -0
- {tilemap_editor-2.0.0 → tilemap_editor-2.0.2}/assets/fonts/jetbrain-fonts/JetBrainsMono-Regular.ttf +0 -0
- {tilemap_editor-2.0.0 → tilemap_editor-2.0.2}/assets/fonts/noto/NotoSans-Bold.ttf +0 -0
- {tilemap_editor-2.0.0 → tilemap_editor-2.0.2}/assets/fonts/noto/NotoSans-BoldItalic.ttf +0 -0
- {tilemap_editor-2.0.0 → tilemap_editor-2.0.2}/assets/fonts/noto/NotoSans-Italic.ttf +0 -0
- {tilemap_editor-2.0.0 → tilemap_editor-2.0.2}/assets/fonts/noto/NotoSans-Regular.ttf +0 -0
- {tilemap_editor-2.0.0 → tilemap_editor-2.0.2}/assets/icons/arrow-down.svg +0 -0
- {tilemap_editor-2.0.0 → tilemap_editor-2.0.2}/assets/icons/check.svg +0 -0
- {tilemap_editor-2.0.0 → tilemap_editor-2.0.2}/assets/icons/checked.svg +0 -0
- {tilemap_editor-2.0.0 → tilemap_editor-2.0.2}/assets/icons/close.svg +0 -0
- {tilemap_editor-2.0.0 → tilemap_editor-2.0.2}/assets/icons/duplicate.svg +0 -0
- {tilemap_editor-2.0.0 → tilemap_editor-2.0.2}/assets/icons/error.svg +0 -0
- {tilemap_editor-2.0.0 → tilemap_editor-2.0.2}/assets/icons/file.svg +0 -0
- {tilemap_editor-2.0.0 → tilemap_editor-2.0.2}/assets/icons/filedead.svg +0 -0
- {tilemap_editor-2.0.0 → tilemap_editor-2.0.2}/assets/icons/fit.svg +0 -0
- {tilemap_editor-2.0.0 → tilemap_editor-2.0.2}/assets/icons/folder.svg +0 -0
- {tilemap_editor-2.0.0 → tilemap_editor-2.0.2}/assets/icons/grid.svg +0 -0
- {tilemap_editor-2.0.0 → tilemap_editor-2.0.2}/assets/icons/image.svg +0 -0
- {tilemap_editor-2.0.0 → tilemap_editor-2.0.2}/assets/icons/info.svg +0 -0
- {tilemap_editor-2.0.0 → tilemap_editor-2.0.2}/assets/icons/load.svg +0 -0
- {tilemap_editor-2.0.0 → tilemap_editor-2.0.2}/assets/icons/loop.svg +0 -0
- {tilemap_editor-2.0.0 → tilemap_editor-2.0.2}/assets/icons/pan.svg +0 -0
- {tilemap_editor-2.0.0 → tilemap_editor-2.0.2}/assets/icons/pause.svg +0 -0
- {tilemap_editor-2.0.0 → tilemap_editor-2.0.2}/assets/icons/pencil.svg +0 -0
- {tilemap_editor-2.0.0 → tilemap_editor-2.0.2}/assets/icons/play.svg +0 -0
- {tilemap_editor-2.0.0 → tilemap_editor-2.0.2}/assets/icons/plus.svg +0 -0
- {tilemap_editor-2.0.0 → tilemap_editor-2.0.2}/assets/icons/radio.svg +0 -0
- {tilemap_editor-2.0.0 → tilemap_editor-2.0.2}/assets/icons/reset.svg +0 -0
- {tilemap_editor-2.0.0 → tilemap_editor-2.0.2}/assets/icons/save.svg +0 -0
- {tilemap_editor-2.0.0 → tilemap_editor-2.0.2}/assets/icons/stop.svg +0 -0
- {tilemap_editor-2.0.0 → tilemap_editor-2.0.2}/assets/icons/tilemap.svg +0 -0
- {tilemap_editor-2.0.0 → tilemap_editor-2.0.2}/assets/icons/tileset.svg +0 -0
- {tilemap_editor-2.0.0 → tilemap_editor-2.0.2}/assets/icons/unchecked.svg +0 -0
- {tilemap_editor-2.0.0 → tilemap_editor-2.0.2}/assets/icons/warning.svg +0 -0
- {tilemap_editor-2.0.0 → tilemap_editor-2.0.2}/assets/icons/zoomin.svg +0 -0
- {tilemap_editor-2.0.0 → tilemap_editor-2.0.2}/assets/icons/zoomout.svg +0 -0
- {tilemap_editor-2.0.0 → tilemap_editor-2.0.2}/setup.cfg +0 -0
- {tilemap_editor-2.0.0 → tilemap_editor-2.0.2}/src/configs/themes.py +0 -0
- {tilemap_editor-2.0.0 → tilemap_editor-2.0.2}/src/constants.py +0 -0
- {tilemap_editor-2.0.0 → tilemap_editor-2.0.2}/src/event_map.py +0 -0
- {tilemap_editor-2.0.0 → tilemap_editor-2.0.2}/src/layers.py +0 -0
- {tilemap_editor-2.0.0 → tilemap_editor-2.0.2}/src/main.py +0 -0
- {tilemap_editor-2.0.0 → tilemap_editor-2.0.2}/src/plugins/__init__.py +0 -0
- {tilemap_editor-2.0.0 → tilemap_editor-2.0.2}/src/plugins/sprite_animation/__init__.py +0 -0
- {tilemap_editor-2.0.0 → tilemap_editor-2.0.2}/src/plugins/sprite_animation/__main__.py +0 -0
- {tilemap_editor-2.0.0 → tilemap_editor-2.0.2}/src/plugins/sprite_animation/clipboard_util.py +0 -0
- {tilemap_editor-2.0.0 → tilemap_editor-2.0.2}/src/plugins/sprite_animation/editor.py +0 -0
- {tilemap_editor-2.0.0 → tilemap_editor-2.0.2}/src/plugins/sprite_animation/frame_picker.py +0 -0
- {tilemap_editor-2.0.0 → tilemap_editor-2.0.2}/src/plugins/sprite_animation/models.py +0 -0
- {tilemap_editor-2.0.0 → tilemap_editor-2.0.2}/src/plugins/sprite_animation/preview.py +0 -0
- {tilemap_editor-2.0.0 → tilemap_editor-2.0.2}/src/plugins/sprite_animation/protocols.py +0 -0
- {tilemap_editor-2.0.0 → tilemap_editor-2.0.2}/src/plugins/sprite_animation/runtime_load.py +0 -0
- {tilemap_editor-2.0.0 → tilemap_editor-2.0.2}/src/plugins/sprite_animation/standalone.py +0 -0
- {tilemap_editor-2.0.0 → tilemap_editor-2.0.2}/src/plugins/sprite_animation/timeline.py +0 -0
- {tilemap_editor-2.0.0 → tilemap_editor-2.0.2}/src/plugins/sprite_animation/validation.py +0 -0
- {tilemap_editor-2.0.0 → tilemap_editor-2.0.2}/src/standalone_automap.py +0 -0
- {tilemap_editor-2.0.0 → tilemap_editor-2.0.2}/src/standalone_error_console.py +0 -0
- {tilemap_editor-2.0.0 → tilemap_editor-2.0.2}/src/standalone_filemanager.py +0 -0
- {tilemap_editor-2.0.0 → tilemap_editor-2.0.2}/src/standalone_image_viewer.py +0 -0
- {tilemap_editor-2.0.0 → tilemap_editor-2.0.2}/src/tilemap.py +0 -0
- {tilemap_editor-2.0.0 → tilemap_editor-2.0.2}/src/tilemap_editor/__init__.py +0 -0
- {tilemap_editor-2.0.0 → tilemap_editor-2.0.2}/src/tilemap_editor/__main__.py +0 -0
- {tilemap_editor-2.0.0 → tilemap_editor-2.0.2}/src/tilemap_editor/cli.py +0 -0
- {tilemap_editor-2.0.0 → tilemap_editor-2.0.2}/src/tilemap_editor.egg-info/dependency_links.txt +0 -0
- {tilemap_editor-2.0.0 → tilemap_editor-2.0.2}/src/tilemap_editor.egg-info/entry_points.txt +0 -0
- {tilemap_editor-2.0.0 → tilemap_editor-2.0.2}/src/tilemap_editor.egg-info/requires.txt +0 -0
- {tilemap_editor-2.0.0 → tilemap_editor-2.0.2}/src/tilemap_editor.egg-info/top_level.txt +0 -0
- {tilemap_editor-2.0.0 → tilemap_editor-2.0.2}/src/ttypes/__init__.py +0 -0
- {tilemap_editor-2.0.0 → tilemap_editor-2.0.2}/src/ttypes/tilemap.py +0 -0
- {tilemap_editor-2.0.0 → tilemap_editor-2.0.2}/src/utils/__init__.py +0 -0
- {tilemap_editor-2.0.0 → tilemap_editor-2.0.2}/src/utils/error_handler.py +0 -0
- {tilemap_editor-2.0.0 → tilemap_editor-2.0.2}/src/utils/font_manager.py +0 -0
- {tilemap_editor-2.0.0 → tilemap_editor-2.0.2}/src/utils/history.py +0 -0
- {tilemap_editor-2.0.0 → tilemap_editor-2.0.2}/src/utils/icon_manager.py +0 -0
- {tilemap_editor-2.0.0 → tilemap_editor-2.0.2}/src/utils/icons_cache.py +0 -0
- {tilemap_editor-2.0.0 → tilemap_editor-2.0.2}/src/utils/log_capture.py +0 -0
- {tilemap_editor-2.0.0 → tilemap_editor-2.0.2}/src/utils/serialization.py +0 -0
- {tilemap_editor-2.0.0 → tilemap_editor-2.0.2}/src/utils/settings.py +0 -0
- {tilemap_editor-2.0.0 → tilemap_editor-2.0.2}/src/utils/standalone.py +0 -0
- {tilemap_editor-2.0.0 → tilemap_editor-2.0.2}/src/utils/validation.py +0 -0
- {tilemap_editor-2.0.0 → tilemap_editor-2.0.2}/src/widgets/__init__.py +0 -0
- {tilemap_editor-2.0.0 → tilemap_editor-2.0.2}/src/widgets/automap_models.py +0 -0
- {tilemap_editor-2.0.0 → tilemap_editor-2.0.2}/src/widgets/autotile_template.py +0 -0
- {tilemap_editor-2.0.0 → tilemap_editor-2.0.2}/src/widgets/filemanager.py +0 -0
- {tilemap_editor-2.0.0 → tilemap_editor-2.0.2}/src/widgets/layer_selector.py +0 -0
- {tilemap_editor-2.0.0 → tilemap_editor-2.0.2}/src/widgets/mapsetup.py +0 -0
- {tilemap_editor-2.0.0 → tilemap_editor-2.0.2}/src/widgets/regex_automap_designer.py +0 -0
- {tilemap_editor-2.0.0 → tilemap_editor-2.0.2}/src/widgets/tile_grid.py +0 -0
- {tilemap_editor-2.0.0 → tilemap_editor-2.0.2}/src/widgets/ui/draw_utils.py +0 -0
- {tilemap_editor-2.0.0 → tilemap_editor-2.0.2}/src/widgets/ui/fileinput.py +0 -0
- {tilemap_editor-2.0.0 → tilemap_editor-2.0.2}/src/widgets/ui/layer_type_dialog.py +0 -0
- {tilemap_editor-2.0.0 → tilemap_editor-2.0.2}/src/widgets/ui/notification.py +0 -0
- {tilemap_editor-2.0.0 → tilemap_editor-2.0.2}/src/widgets/ui/property_editor.py +0 -0
- {tilemap_editor-2.0.0 → tilemap_editor-2.0.2}/src/widgets/ui/theme.py +0 -0
- {tilemap_editor-2.0.0 → tilemap_editor-2.0.2}/src/widgets/ui/tileset_type_dialog.py +0 -0
- {tilemap_editor-2.0.0 → tilemap_editor-2.0.2}/src/widgets/ui/toolbar.py +0 -0
- {tilemap_editor-2.0.0 → tilemap_editor-2.0.2}/src/widgets/ui/tooltip.py +0 -0
|
@@ -5,6 +5,25 @@ All notable changes to this project will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [2.0.1] - 2025-04-23
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
- **Tileset Collision Plugin**: Godot-like polygon collision editor
|
|
12
|
+
- `CollisionPainter`: Interactive polygon drawing with edge-constrain mode
|
|
13
|
+
- `TilesetCollisionEditor`: Full editor with tile selector and painted tiles list
|
|
14
|
+
- `TilesetCollisionLibrary`: Persistent collision data management
|
|
15
|
+
- Edge-draw mode (`E` key + `Shift`) for precise slope/stair creation
|
|
16
|
+
- Help panel (`H` key) with all controls documented
|
|
17
|
+
- One-way collision support for platforms
|
|
18
|
+
- Zoom, pan, and grid controls
|
|
19
|
+
- **Character Collision Plugin**: Shape-based character collision editor
|
|
20
|
+
- Rectangle, Circle, and Capsule shape support
|
|
21
|
+
- Protocol-based design for easy integration
|
|
22
|
+
|
|
23
|
+
### Changed
|
|
24
|
+
- Updated pyproject.toml version to 2.0.1
|
|
25
|
+
- Improved editor.py and tile_selector.py integration
|
|
26
|
+
|
|
8
27
|
## [2.0.0] - 2025-04-22
|
|
9
28
|
|
|
10
29
|
### Major Changes
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: tilemap-editor
|
|
3
|
-
Version: 2.0.
|
|
3
|
+
Version: 2.0.2
|
|
4
4
|
Summary: Pygame tilemap editor with SVG icons, professional UI, and sprite animation tools
|
|
5
5
|
Author: tilemap editor contributors
|
|
6
6
|
License: Proprietary
|
|
@@ -16,25 +16,29 @@ Dynamic: license-file
|
|
|
16
16
|
|
|
17
17
|
# tilemap-editor
|
|
18
18
|
|
|
19
|
+
> [!WARNING]
|
|
20
|
+
> This is mostly vibe coded as working with ui isnt so easy in pygame. Somehow it works for me. Though it took decent time as this was initially too buggy(which still is as expected). Just an experiment and keep customizing for my need.
|
|
21
|
+
PRs are welcome.
|
|
22
|
+
|
|
19
23
|
`tilemap-editor` is a pygame-based map editor focused on fast iteration for 2D games.
|
|
24
|
+
`tilemap-parser` to parse this editor. (https://pypi.org/project/tilemap-parser/)[https://pypi.org/project/tilemap-parser/]
|
|
20
25
|
|
|
21
26
|
## What it includes
|
|
22
27
|
|
|
23
28
|
- **Multi-layer tile/object map editing** with visual layer management
|
|
24
|
-
- **
|
|
29
|
+
- **Decent enough UI** with SVG icons, consistent fonts, and modern design
|
|
25
30
|
- **Sprite animation editor** (`tilemap-anim-editor`) with frame-by-frame editing
|
|
26
31
|
- **Built-in tools**: File manager, error console, image viewer, automapper
|
|
27
32
|
- **Font management** with auto-selected coding fonts for clarity
|
|
28
33
|
- **Icon system** with 26+ SVG icons and geometric fallbacks
|
|
29
34
|
- **JSON save/load** compatible with this editor
|
|
35
|
+
- **Decent console** toggled with ctrl + ` for error/warning/logs/debugging
|
|
30
36
|
|
|
31
37
|
## Key Features
|
|
32
38
|
|
|
33
39
|
### UI Improvements
|
|
34
|
-
-
|
|
40
|
+
- SVG icons throughout the interface
|
|
35
41
|
- Bold, auto-selected coding fonts (JetBrains Mono, Fira Code, Consolas)
|
|
36
|
-
- Professional color scheme and consistent styling
|
|
37
|
-
- Dynamic icon loading - icons auto-load by button name
|
|
38
42
|
|
|
39
43
|
### Animation Editor
|
|
40
44
|
- Frame picker with visual thumbnails
|
|
@@ -52,8 +56,53 @@ Dynamic: license-file
|
|
|
52
56
|
|
|
53
57
|
## Install (local/dev)
|
|
54
58
|
|
|
59
|
+
### Using Virtual Environment (Recommended)
|
|
60
|
+
|
|
61
|
+
For Linux and macOS:
|
|
62
|
+
|
|
55
63
|
```bash
|
|
64
|
+
# Create virtual environment
|
|
65
|
+
python3 -m venv .venv
|
|
66
|
+
|
|
67
|
+
# Activate virtual environment
|
|
68
|
+
source .venv/bin/activate
|
|
69
|
+
|
|
70
|
+
# Install in development mode
|
|
71
|
+
pip install -e .
|
|
72
|
+
|
|
73
|
+
# Deactivate when done
|
|
74
|
+
deactivate
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
For Windows:
|
|
78
|
+
|
|
79
|
+
```bash
|
|
80
|
+
# Create virtual environment
|
|
81
|
+
python -m venv .venv
|
|
82
|
+
|
|
83
|
+
# Activate virtual environment
|
|
84
|
+
.venv\Scripts\activate
|
|
85
|
+
|
|
86
|
+
# Install in development mode
|
|
56
87
|
pip install -e .
|
|
88
|
+
|
|
89
|
+
# Deactivate when done
|
|
90
|
+
deactivate
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
### Direct Install (Not Recommended)
|
|
94
|
+
|
|
95
|
+
```bash
|
|
96
|
+
pip install -e .
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
## Package installtion from pypi
|
|
100
|
+
|
|
101
|
+
```bash
|
|
102
|
+
pip install tilemap-editor
|
|
103
|
+
```
|
|
104
|
+
```bash
|
|
105
|
+
pip install tilemap-parser
|
|
57
106
|
```
|
|
58
107
|
|
|
59
108
|
## Run editor
|
|
@@ -1,24 +1,28 @@
|
|
|
1
1
|
# tilemap-editor
|
|
2
2
|
|
|
3
|
+
> [!WARNING]
|
|
4
|
+
> This is mostly vibe coded as working with ui isnt so easy in pygame. Somehow it works for me. Though it took decent time as this was initially too buggy(which still is as expected). Just an experiment and keep customizing for my need.
|
|
5
|
+
PRs are welcome.
|
|
6
|
+
|
|
3
7
|
`tilemap-editor` is a pygame-based map editor focused on fast iteration for 2D games.
|
|
8
|
+
`tilemap-parser` to parse this editor. (https://pypi.org/project/tilemap-parser/)[https://pypi.org/project/tilemap-parser/]
|
|
4
9
|
|
|
5
10
|
## What it includes
|
|
6
11
|
|
|
7
12
|
- **Multi-layer tile/object map editing** with visual layer management
|
|
8
|
-
- **
|
|
13
|
+
- **Decent enough UI** with SVG icons, consistent fonts, and modern design
|
|
9
14
|
- **Sprite animation editor** (`tilemap-anim-editor`) with frame-by-frame editing
|
|
10
15
|
- **Built-in tools**: File manager, error console, image viewer, automapper
|
|
11
16
|
- **Font management** with auto-selected coding fonts for clarity
|
|
12
17
|
- **Icon system** with 26+ SVG icons and geometric fallbacks
|
|
13
18
|
- **JSON save/load** compatible with this editor
|
|
19
|
+
- **Decent console** toggled with ctrl + ` for error/warning/logs/debugging
|
|
14
20
|
|
|
15
21
|
## Key Features
|
|
16
22
|
|
|
17
23
|
### UI Improvements
|
|
18
|
-
-
|
|
24
|
+
- SVG icons throughout the interface
|
|
19
25
|
- Bold, auto-selected coding fonts (JetBrains Mono, Fira Code, Consolas)
|
|
20
|
-
- Professional color scheme and consistent styling
|
|
21
|
-
- Dynamic icon loading - icons auto-load by button name
|
|
22
26
|
|
|
23
27
|
### Animation Editor
|
|
24
28
|
- Frame picker with visual thumbnails
|
|
@@ -36,8 +40,53 @@
|
|
|
36
40
|
|
|
37
41
|
## Install (local/dev)
|
|
38
42
|
|
|
43
|
+
### Using Virtual Environment (Recommended)
|
|
44
|
+
|
|
45
|
+
For Linux and macOS:
|
|
46
|
+
|
|
39
47
|
```bash
|
|
48
|
+
# Create virtual environment
|
|
49
|
+
python3 -m venv .venv
|
|
50
|
+
|
|
51
|
+
# Activate virtual environment
|
|
52
|
+
source .venv/bin/activate
|
|
53
|
+
|
|
54
|
+
# Install in development mode
|
|
55
|
+
pip install -e .
|
|
56
|
+
|
|
57
|
+
# Deactivate when done
|
|
58
|
+
deactivate
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
For Windows:
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
# Create virtual environment
|
|
65
|
+
python -m venv .venv
|
|
66
|
+
|
|
67
|
+
# Activate virtual environment
|
|
68
|
+
.venv\Scripts\activate
|
|
69
|
+
|
|
70
|
+
# Install in development mode
|
|
40
71
|
pip install -e .
|
|
72
|
+
|
|
73
|
+
# Deactivate when done
|
|
74
|
+
deactivate
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
### Direct Install (Not Recommended)
|
|
78
|
+
|
|
79
|
+
```bash
|
|
80
|
+
pip install -e .
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
## Package installtion from pypi
|
|
84
|
+
|
|
85
|
+
```bash
|
|
86
|
+
pip install tilemap-editor
|
|
87
|
+
```
|
|
88
|
+
```bash
|
|
89
|
+
pip install tilemap-parser
|
|
41
90
|
```
|
|
42
91
|
|
|
43
92
|
## Run editor
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "tilemap-editor"
|
|
7
|
-
version = "2.0.
|
|
7
|
+
version = "2.0.2"
|
|
8
8
|
description = "Pygame tilemap editor with SVG icons, professional UI, and sprite animation tools"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.10"
|
|
@@ -490,6 +490,9 @@ class Editor:
|
|
|
490
490
|
self._init_animation_panel()
|
|
491
491
|
if self.animation_panel is not None:
|
|
492
492
|
self.left_panel_visible = True
|
|
493
|
+
else:
|
|
494
|
+
self.left_panel_visible = False
|
|
495
|
+
|
|
493
496
|
|
|
494
497
|
def _init_animation_panel(self):
|
|
495
498
|
"""Initialize the animation panel without loading any tileset."""
|
|
@@ -605,6 +608,87 @@ class Editor:
|
|
|
605
608
|
except Exception as e:
|
|
606
609
|
error_handler.capture(e, context="launch_animation_editor")
|
|
607
610
|
|
|
611
|
+
def launch_collision_editor(self):
|
|
612
|
+
"""Launch the tileset collision editor in a new window.
|
|
613
|
+
|
|
614
|
+
Uses the active tileset image when one is loaded. If no tileset is
|
|
615
|
+
selected, shows a notification.
|
|
616
|
+
"""
|
|
617
|
+
sheet = self._active_tileset_image_path()
|
|
618
|
+
if sheet is not None:
|
|
619
|
+
self._launch_collision_editor_with_image(sheet)
|
|
620
|
+
return
|
|
621
|
+
|
|
622
|
+
self.notifications.notify("No tileset loaded. Please load a tileset first.")
|
|
623
|
+
|
|
624
|
+
def _launch_collision_editor_with_image(self, path: Path):
|
|
625
|
+
"""Launch collision editor subprocess with selected tileset."""
|
|
626
|
+
try:
|
|
627
|
+
tile_size = "32x32"
|
|
628
|
+
if hasattr(self.tilemap, "tile_size") and self.tilemap.tile_size:
|
|
629
|
+
tw, th = self.tilemap.tile_size
|
|
630
|
+
tile_size = f"{tw}x{th}"
|
|
631
|
+
|
|
632
|
+
args = [str(path), "--tile-size", tile_size]
|
|
633
|
+
|
|
634
|
+
# Check if collision data file exists and load it
|
|
635
|
+
collision_path = path.with_suffix('.collision.json')
|
|
636
|
+
if collision_path.exists():
|
|
637
|
+
args.extend(["--load", str(collision_path)])
|
|
638
|
+
|
|
639
|
+
process = launch_standalone(
|
|
640
|
+
"plugins.tileset_collision.standalone",
|
|
641
|
+
args,
|
|
642
|
+
cwd=BASE_PATH,
|
|
643
|
+
text=True,
|
|
644
|
+
)
|
|
645
|
+
self.child_processes.append(process)
|
|
646
|
+
|
|
647
|
+
print(
|
|
648
|
+
f"Launched collision editor with: {path.name} (tile size: {tile_size})"
|
|
649
|
+
)
|
|
650
|
+
except Exception as e:
|
|
651
|
+
error_handler.capture(e, context="launch_collision_editor")
|
|
652
|
+
|
|
653
|
+
def launch_character_collision_editor(self):
|
|
654
|
+
"""Launch the character collision editor in a new window.
|
|
655
|
+
|
|
656
|
+
Opens a file picker to select a character sprite image.
|
|
657
|
+
"""
|
|
658
|
+
self.open_file_manager(
|
|
659
|
+
on_select=self._launch_character_collision_editor_with_image,
|
|
660
|
+
initial_dir=BASE_PATH / "data",
|
|
661
|
+
allowed_exts=[".png", ".jpg", ".jpeg"],
|
|
662
|
+
mode="open",
|
|
663
|
+
)
|
|
664
|
+
|
|
665
|
+
def _launch_character_collision_editor_with_image(self, path: Path):
|
|
666
|
+
"""Launch character collision editor subprocess with selected image."""
|
|
667
|
+
try:
|
|
668
|
+
# Use filename (without extension) as default character name
|
|
669
|
+
character_name = path.stem
|
|
670
|
+
|
|
671
|
+
args = [str(path), "--name", character_name]
|
|
672
|
+
|
|
673
|
+
# Check if collision data file exists and load it
|
|
674
|
+
collision_path = path.with_suffix('.collision.json')
|
|
675
|
+
if collision_path.exists():
|
|
676
|
+
args.extend(["--load", str(collision_path)])
|
|
677
|
+
|
|
678
|
+
process = launch_standalone(
|
|
679
|
+
"plugins.character_collision.standalone",
|
|
680
|
+
args,
|
|
681
|
+
cwd=BASE_PATH,
|
|
682
|
+
text=True,
|
|
683
|
+
)
|
|
684
|
+
self.child_processes.append(process)
|
|
685
|
+
|
|
686
|
+
print(
|
|
687
|
+
f"Launched character collision editor with: {path.name} (character: {character_name})"
|
|
688
|
+
)
|
|
689
|
+
except Exception as e:
|
|
690
|
+
error_handler.capture(e, context="launch_character_collision_editor")
|
|
691
|
+
|
|
608
692
|
def launch_error_console(self):
|
|
609
693
|
"""Launch the error console as a subprocess."""
|
|
610
694
|
if self.error_console_process and self.error_console_process.poll() is None:
|
|
@@ -670,6 +754,7 @@ class Editor:
|
|
|
670
754
|
self.child_processes = [p for p in self.child_processes if p.poll() is None]
|
|
671
755
|
|
|
672
756
|
def handle_events(self):
|
|
757
|
+
"""Process all pygame events."""
|
|
673
758
|
for event in pygame.event.get():
|
|
674
759
|
if event.type == pygame.QUIT:
|
|
675
760
|
self.running = False
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Character Collision Editor Plugin
|
|
3
|
+
|
|
4
|
+
Editor for defining collision shapes for character sprites.
|
|
5
|
+
Supports rectangle, circle, capsule, and polygon shapes.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from .models import (
|
|
9
|
+
CharacterCollisionData,
|
|
10
|
+
RectangleCollisionData,
|
|
11
|
+
CircleCollisionData,
|
|
12
|
+
CapsuleCollisionData,
|
|
13
|
+
PolygonCollisionData,
|
|
14
|
+
)
|
|
15
|
+
from .protocols import CollisionShapeType
|
|
16
|
+
|
|
17
|
+
__all__ = [
|
|
18
|
+
"CharacterCollisionData",
|
|
19
|
+
"RectangleCollisionData",
|
|
20
|
+
"CircleCollisionData",
|
|
21
|
+
"CapsuleCollisionData",
|
|
22
|
+
"PolygonCollisionData",
|
|
23
|
+
"CollisionShapeType",
|
|
24
|
+
]
|
|
@@ -0,0 +1,320 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Character Collision Editor — Define collision shapes for character sprites.
|
|
3
|
+
|
|
4
|
+
Layout:
|
|
5
|
+
+----------------------------------------------------+
|
|
6
|
+
| Toolbar: [Character Name] [Shape Type Buttons] |
|
|
7
|
+
+----------------------------------------------------+
|
|
8
|
+
| |
|
|
9
|
+
| Shape Editor |
|
|
10
|
+
| (visual shape editing) |
|
|
11
|
+
| |
|
|
12
|
+
+----------------------------------------------------+
|
|
13
|
+
| Properties: [Width/Height/Radius/etc.] |
|
|
14
|
+
+----------------------------------------------------+
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
from typing import Optional, Tuple, Dict, Any
|
|
20
|
+
from pathlib import Path
|
|
21
|
+
|
|
22
|
+
import pygame
|
|
23
|
+
from pygame import Rect, Surface
|
|
24
|
+
|
|
25
|
+
from .models import CharacterCollisionData
|
|
26
|
+
from .shape_editor import ShapeEditor, ShapeType
|
|
27
|
+
from utils.font_manager import font_manager, FontWeight
|
|
28
|
+
from utils.icon_manager import icon_manager
|
|
29
|
+
from utils.error_handler import error_handler, error_context
|
|
30
|
+
from widgets.ui.theme import COLORS, FONTS
|
|
31
|
+
from widgets.ui.draw_utils import draw_panel, draw_button
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class CharacterCollisionEditor:
|
|
35
|
+
"""Main editor for character collision shapes."""
|
|
36
|
+
|
|
37
|
+
def __init__(
|
|
38
|
+
self,
|
|
39
|
+
rect: Rect,
|
|
40
|
+
sprite_surface: Surface,
|
|
41
|
+
character_name: str = "Character",
|
|
42
|
+
):
|
|
43
|
+
self.rect = rect
|
|
44
|
+
self.sprite_surface = sprite_surface
|
|
45
|
+
self.character_name = character_name
|
|
46
|
+
self.visible = True
|
|
47
|
+
|
|
48
|
+
# UI layout
|
|
49
|
+
self.toolbar_height = 50
|
|
50
|
+
self.properties_height = 100
|
|
51
|
+
|
|
52
|
+
shape_editor_rect = Rect(
|
|
53
|
+
rect.x,
|
|
54
|
+
rect.y + self.toolbar_height,
|
|
55
|
+
rect.w,
|
|
56
|
+
rect.h - self.toolbar_height - self.properties_height
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
# Shape editor
|
|
60
|
+
self.shape_editor = ShapeEditor(shape_editor_rect, sprite_surface)
|
|
61
|
+
|
|
62
|
+
# Fonts
|
|
63
|
+
self._font = font_manager.get_font(FONTS.name, FONTS.size_md, FontWeight.REGULAR)
|
|
64
|
+
self._font_sm = font_manager.get_font(FONTS.name, FONTS.size_sm, FontWeight.REGULAR)
|
|
65
|
+
|
|
66
|
+
# Shape type buttons
|
|
67
|
+
self.shape_buttons: Dict[ShapeType, Rect] = {}
|
|
68
|
+
self._init_shape_buttons()
|
|
69
|
+
|
|
70
|
+
def _init_shape_buttons(self) -> None:
|
|
71
|
+
"""Initialize shape type button rects"""
|
|
72
|
+
button_w = 100
|
|
73
|
+
button_h = 30
|
|
74
|
+
button_spacing = 10
|
|
75
|
+
start_x = self.rect.x + 250
|
|
76
|
+
|
|
77
|
+
shapes: list[ShapeType] = ["rectangle", "circle", "capsule"]
|
|
78
|
+
for i, shape in enumerate(shapes):
|
|
79
|
+
x = start_x + i * (button_w + button_spacing)
|
|
80
|
+
y = self.rect.y + 10
|
|
81
|
+
self.shape_buttons[shape] = Rect(x, y, button_w, button_h)
|
|
82
|
+
|
|
83
|
+
def resize(self, rect: Rect) -> None:
|
|
84
|
+
"""Resize the editor"""
|
|
85
|
+
self.rect = rect
|
|
86
|
+
|
|
87
|
+
shape_editor_rect = Rect(
|
|
88
|
+
rect.x,
|
|
89
|
+
rect.y + self.toolbar_height,
|
|
90
|
+
rect.w,
|
|
91
|
+
rect.h - self.toolbar_height - self.properties_height
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
self.shape_editor.resize(shape_editor_rect)
|
|
95
|
+
self._init_shape_buttons()
|
|
96
|
+
|
|
97
|
+
def get_collision_data(self) -> CharacterCollisionData:
|
|
98
|
+
"""Get the current collision data"""
|
|
99
|
+
shape_data = self.shape_editor.get_shape_data()
|
|
100
|
+
|
|
101
|
+
# Import shape data classes
|
|
102
|
+
from .models import (
|
|
103
|
+
RectangleCollisionData,
|
|
104
|
+
CircleCollisionData,
|
|
105
|
+
CapsuleCollisionData,
|
|
106
|
+
PolygonCollisionData,
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
shape_type = shape_data["type"]
|
|
110
|
+
if shape_type == "rectangle":
|
|
111
|
+
shape = RectangleCollisionData(
|
|
112
|
+
width=shape_data["width"],
|
|
113
|
+
height=shape_data["height"],
|
|
114
|
+
offset=shape_data["offset"],
|
|
115
|
+
)
|
|
116
|
+
elif shape_type == "circle":
|
|
117
|
+
shape = CircleCollisionData(
|
|
118
|
+
radius=shape_data["radius"],
|
|
119
|
+
offset=shape_data["offset"],
|
|
120
|
+
)
|
|
121
|
+
elif shape_type == "capsule":
|
|
122
|
+
shape = CapsuleCollisionData(
|
|
123
|
+
radius=shape_data["radius"],
|
|
124
|
+
height=shape_data["height"],
|
|
125
|
+
offset=shape_data["offset"],
|
|
126
|
+
)
|
|
127
|
+
elif shape_type == "polygon":
|
|
128
|
+
shape = PolygonCollisionData(
|
|
129
|
+
vertices=shape_data["vertices"],
|
|
130
|
+
offset=shape_data["offset"],
|
|
131
|
+
)
|
|
132
|
+
else:
|
|
133
|
+
raise ValueError(f"Unknown shape type: {shape_type}")
|
|
134
|
+
|
|
135
|
+
return CharacterCollisionData(
|
|
136
|
+
name=self.character_name,
|
|
137
|
+
shape=shape,
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
def load_collision_data(self, data: CharacterCollisionData) -> None:
|
|
141
|
+
"""Load collision data"""
|
|
142
|
+
self.character_name = data.name
|
|
143
|
+
self.shape_editor.load_shape_data(data.shape.to_dict())
|
|
144
|
+
|
|
145
|
+
def save_to_file(self, path: Path) -> None:
|
|
146
|
+
"""Save collision data to file"""
|
|
147
|
+
try:
|
|
148
|
+
import json
|
|
149
|
+
data = self.get_collision_data()
|
|
150
|
+
with open(path, 'w') as f:
|
|
151
|
+
json.dump(data.to_dict(), f, indent=2)
|
|
152
|
+
except Exception as e:
|
|
153
|
+
error_handler.capture(e, context="save_character_collision")
|
|
154
|
+
|
|
155
|
+
def load_from_file(self, path: Path) -> None:
|
|
156
|
+
"""Load collision data from file"""
|
|
157
|
+
try:
|
|
158
|
+
import json
|
|
159
|
+
with open(path, 'r') as f:
|
|
160
|
+
data_dict = json.load(f)
|
|
161
|
+
data = CharacterCollisionData.from_dict(data_dict)
|
|
162
|
+
self.load_collision_data(data)
|
|
163
|
+
except Exception as e:
|
|
164
|
+
error_handler.capture(e, context="load_character_collision")
|
|
165
|
+
|
|
166
|
+
def handle_event(self, event: pygame.event.Event) -> bool:
|
|
167
|
+
"""Handle input events"""
|
|
168
|
+
if not self.visible:
|
|
169
|
+
return False
|
|
170
|
+
|
|
171
|
+
# Let shape editor handle events first
|
|
172
|
+
if self.shape_editor.handle_event(event):
|
|
173
|
+
return True
|
|
174
|
+
|
|
175
|
+
mouse = pygame.mouse.get_pos()
|
|
176
|
+
|
|
177
|
+
# Shape type button clicks
|
|
178
|
+
if event.type == pygame.MOUSEBUTTONDOWN and event.button == 1:
|
|
179
|
+
for shape_type, button_rect in self.shape_buttons.items():
|
|
180
|
+
if button_rect.collidepoint(mouse):
|
|
181
|
+
self.shape_editor.set_shape_type(shape_type)
|
|
182
|
+
return True
|
|
183
|
+
|
|
184
|
+
return False
|
|
185
|
+
|
|
186
|
+
def draw(self, screen: Surface) -> None:
|
|
187
|
+
"""Draw the editor"""
|
|
188
|
+
if not self.visible:
|
|
189
|
+
return
|
|
190
|
+
|
|
191
|
+
# Draw toolbar
|
|
192
|
+
self._draw_toolbar(screen)
|
|
193
|
+
|
|
194
|
+
# Draw shape editor
|
|
195
|
+
self.shape_editor.draw(screen)
|
|
196
|
+
|
|
197
|
+
# Draw properties panel
|
|
198
|
+
self._draw_properties(screen)
|
|
199
|
+
|
|
200
|
+
def _draw_toolbar(self, screen: Surface) -> None:
|
|
201
|
+
"""Draw the toolbar"""
|
|
202
|
+
toolbar_rect = Rect(self.rect.x, self.rect.y, self.rect.w, self.toolbar_height)
|
|
203
|
+
draw_panel(screen, toolbar_rect, COLORS.header, COLORS.border)
|
|
204
|
+
|
|
205
|
+
# Title
|
|
206
|
+
title = self._font.render(
|
|
207
|
+
f"Character Collision Editor — {self.character_name}",
|
|
208
|
+
True,
|
|
209
|
+
COLORS.text
|
|
210
|
+
)
|
|
211
|
+
screen.blit(title, (toolbar_rect.x + 10, toolbar_rect.y + 15))
|
|
212
|
+
|
|
213
|
+
# Shape type buttons
|
|
214
|
+
for shape_type, button_rect in self.shape_buttons.items():
|
|
215
|
+
is_active = (self.shape_editor.shape_type == shape_type)
|
|
216
|
+
|
|
217
|
+
# Render label
|
|
218
|
+
label_surf = self._font_sm.render(shape_type.capitalize(), True, COLORS.text)
|
|
219
|
+
|
|
220
|
+
draw_button(screen, button_rect, label_surf, active=is_active)
|
|
221
|
+
|
|
222
|
+
def _draw_properties(self, screen: Surface) -> None:
|
|
223
|
+
"""Draw the properties panel"""
|
|
224
|
+
props_rect = Rect(
|
|
225
|
+
self.rect.x,
|
|
226
|
+
self.rect.bottom - self.properties_height,
|
|
227
|
+
self.rect.w,
|
|
228
|
+
self.properties_height
|
|
229
|
+
)
|
|
230
|
+
draw_panel(screen, props_rect, COLORS.panel, COLORS.border)
|
|
231
|
+
|
|
232
|
+
# Display current shape properties
|
|
233
|
+
shape_data = self.shape_editor.get_shape_data()
|
|
234
|
+
y = props_rect.y + 10
|
|
235
|
+
x = props_rect.x + 10
|
|
236
|
+
|
|
237
|
+
title = self._font.render("Properties:", True, COLORS.text)
|
|
238
|
+
screen.blit(title, (x, y))
|
|
239
|
+
y += 25
|
|
240
|
+
|
|
241
|
+
if shape_data["type"] == "rectangle":
|
|
242
|
+
text = self._font_sm.render(
|
|
243
|
+
f"Width: {shape_data['width']:.1f} Height: {shape_data['height']:.1f} Offset: ({shape_data['offset'][0]:.1f}, {shape_data['offset'][1]:.1f})",
|
|
244
|
+
True,
|
|
245
|
+
COLORS.text_dim
|
|
246
|
+
)
|
|
247
|
+
screen.blit(text, (x, y))
|
|
248
|
+
elif shape_data["type"] == "circle":
|
|
249
|
+
text = self._font_sm.render(
|
|
250
|
+
f"Radius: {shape_data['radius']:.1f} Offset: ({shape_data['offset'][0]:.1f}, {shape_data['offset'][1]:.1f})",
|
|
251
|
+
True,
|
|
252
|
+
COLORS.text_dim
|
|
253
|
+
)
|
|
254
|
+
screen.blit(text, (x, y))
|
|
255
|
+
elif shape_data["type"] == "capsule":
|
|
256
|
+
text = self._font_sm.render(
|
|
257
|
+
f"Radius: {shape_data['radius']:.1f} Height: {shape_data['height']:.1f} Offset: ({shape_data['offset'][0]:.1f}, {shape_data['offset'][1]:.1f})",
|
|
258
|
+
True,
|
|
259
|
+
COLORS.text_dim
|
|
260
|
+
)
|
|
261
|
+
screen.blit(text, (x, y))
|
|
262
|
+
elif shape_data["type"] == "polygon":
|
|
263
|
+
vertex_count = len(shape_data["vertices"])
|
|
264
|
+
text = self._font_sm.render(
|
|
265
|
+
f"Vertices: {vertex_count}",
|
|
266
|
+
True,
|
|
267
|
+
COLORS.text_dim
|
|
268
|
+
)
|
|
269
|
+
screen.blit(text, (x, y))
|
|
270
|
+
|
|
271
|
+
@classmethod
|
|
272
|
+
def from_path(
|
|
273
|
+
cls,
|
|
274
|
+
sprite_path: Path,
|
|
275
|
+
window_size: Tuple[int, int] = (1000, 800),
|
|
276
|
+
character_name: str = "Character",
|
|
277
|
+
) -> "CharacterCollisionEditor":
|
|
278
|
+
"""Create editor from sprite image path (for standalone use)"""
|
|
279
|
+
surface = pygame.image.load(sprite_path).convert_alpha()
|
|
280
|
+
rect = Rect(0, 0, window_size[0], window_size[1])
|
|
281
|
+
return cls(rect, surface, character_name)
|
|
282
|
+
|
|
283
|
+
def run(self) -> None:
|
|
284
|
+
"""Run standalone editor (for standalone use)"""
|
|
285
|
+
screen = pygame.display.get_surface()
|
|
286
|
+
if screen is None:
|
|
287
|
+
raise RuntimeError("pygame display not initialized")
|
|
288
|
+
|
|
289
|
+
clock = pygame.time.Clock()
|
|
290
|
+
running = True
|
|
291
|
+
|
|
292
|
+
while running:
|
|
293
|
+
for event in pygame.event.get():
|
|
294
|
+
if event.type == pygame.QUIT:
|
|
295
|
+
running = False
|
|
296
|
+
elif event.type == pygame.KEYDOWN:
|
|
297
|
+
if event.key == pygame.K_ESCAPE:
|
|
298
|
+
running = False
|
|
299
|
+
elif event.key == pygame.K_s and (
|
|
300
|
+
pygame.key.get_mods() & (pygame.KMOD_LCTRL | pygame.KMOD_LMETA)
|
|
301
|
+
):
|
|
302
|
+
# Ctrl+S / Cmd+S to save
|
|
303
|
+
save_path = Path("character_collision.json")
|
|
304
|
+
self.save_to_file(save_path)
|
|
305
|
+
print(f"Saved collision data to {save_path}")
|
|
306
|
+
elif event.key == pygame.K_l and (
|
|
307
|
+
pygame.key.get_mods() & (pygame.KMOD_LCTRL | pygame.KMOD_LMETA)
|
|
308
|
+
):
|
|
309
|
+
# Ctrl+L / Cmd+L to load
|
|
310
|
+
load_path = Path("character_collision.json")
|
|
311
|
+
if load_path.exists():
|
|
312
|
+
self.load_from_file(load_path)
|
|
313
|
+
print(f"Loaded collision data from {load_path}")
|
|
314
|
+
|
|
315
|
+
self.handle_event(event)
|
|
316
|
+
|
|
317
|
+
screen.fill((20, 20, 20))
|
|
318
|
+
self.draw(screen)
|
|
319
|
+
pygame.display.flip()
|
|
320
|
+
clock.tick(60)
|