Metadata-Version: 2.4
Name: walimaker
Version: 3.1.2
Summary: A Python game engine designed for educational purposes
Author-email: Walimaker Project <walimaker@example.com>
Maintainer-email: Walimaker Maintainers <walimaker@example.com>
License-Expression: MIT
Project-URL: PyPI, https://pypi.org/project/walimaker/
Keywords: game,pygame,pymunk,education,game-engine
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Education
Classifier: Intended Audience :: Developers
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Classifier: Topic :: Education
Classifier: Topic :: Games/Entertainment
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.8
Classifier: Programming Language :: Python :: 3.9
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Operating System :: OS Independent
Classifier: Environment :: MacOS X
Classifier: Environment :: Win32 (MS Windows)
Classifier: Environment :: X11 Applications
Requires-Python: >=3.8.1
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: pygame>=2.5.0
Requires-Dist: pymunk>=6.5.0
Requires-Dist: pytmx>=3.32
Provides-Extra: dev
Requires-Dist: black>=23.0; extra == "dev"
Requires-Dist: flake8>=6.0; extra == "dev"
Requires-Dist: mypy>=1.0; extra == "dev"
Requires-Dist: pytest>=7.0; extra == "dev"
Requires-Dist: twine>=4.0; extra == "dev"
Provides-Extra: docs
Requires-Dist: mkdocs>=1.4; extra == "docs"
Requires-Dist: mkdocs-material>=9.0; extra == "docs"
Dynamic: license-file

# 🎮 Walimaker - 专为教学设计的Python游戏引擎

[![Python Version](https://img.shields.io/badge/python-3.8+-blue.svg)](https://www.python.org/downloads/)
[![License](https://img.shields.io/badge/license-MIT-green.svg)](LICENSE)
[![Code Style](https://img.shields.io/badge/code%20style-black-black.svg)](https://github.com/psf/black)

<p align="center">
  <img src="./src/Walimaker/static/walimaker-logo.png" alt="Walimaker Logo" width="200"/>
</p>

> **Walimaker** 是一个专为教学场景设计的Python游戏引擎模块。它采用简洁直观的API设计，让学生能够快速上手游戏开发，同时掌握编程核心概念。模块基于Pygame和Pymunk构建，提供完整的2D游戏开发功能。

## 📖 目录

- [✨ 特性亮点](#-特性亮点)｜[🚀 快速开始](#-快速开始)｜[🧭 中文报错提示](#-中文报错提示教学特性)
- [📁 项目结构](#-项目结构)｜[🧰 本地开发与测试](#-本地开发与测试uv推荐)
- [📖 API 参考](#-api-参考)：窗口与主循环、Character/对象模型、物理引擎、约束、相机、TileMap、Sprite、TextBox、输入、音频、资源、报错助手、字体、世界与全局状态、场景级工具
- [🚦 高级用法](#-高级用法)：多世界、相机手感、防隧穿、约束、TileMap、动画状态机、资源管理、中文字体
- [🍳 常见任务速查](#-常见任务速查cookbook)｜[📊 性能优化建议](#-性能优化建议)｜[❓ 常见问题](#-常见问题faq)
- [🧪 示例代码](#-示例代码)｜[🚀 下一步计划](#-下一步计划roadmap)｜[🧭 坐标系说明](#-坐标系说明)
- [🤝 贡献指南](#-贡献指南)｜[📄 许可证](#-许可证)｜[📞 支持与反馈](#-支持与反馈)

## ✨ 特性亮点

### 🎯 教学友好
- **简洁API**：基于Python语法糖，类似Processing/turtle的直观接口
- **错误友好**：详尽的错误提示和调试信息，帮助初学者快速定位问题
- **渐进学习**：从简单绘图到复杂游戏逻辑的平滑过渡

### 🛠 功能完备
- **🎨 图形渲染**：精灵动画、位图渲染、TileMap支持
- **⚙️ 物理系统**：刚体动力学、碰撞检测、约束（Pymunk：`connect()` 刚性杆、`Spring()` 弹簧）
- **🎵 音频管理**：背景音乐、音效播放、音量控制
- **📝 UI组件**：文本框、对话框
- **⌨️ 输入系统**：鼠标与键盘事件
- **📊 资源管理**：智能 LRU 缓存，自动内存管理

### 🚀 开发效率
- **调试工具**：调试绘制 `debug()`、自动刷新 `tracer()`、移动限速 `speed()`、性能基准脚本
- **类型友好**：随包提供 `py.typed`，编辑器与 mypy 能直接提示参数类型
- **测试与门禁**：8 个测试文件 + `scripts/check.py`（一条命令跑 black / flake8 / mypy / 全部测试，
  并支持 `--matrix` 自动跑 pymunk 6/7 双版本）
- **模块化设计**：对象模型、精灵动画、物理、渲染、资源各司其职，便于定制

## 🚀 快速开始

### 安装

#### 通过pip安装
```bash
pip install Walimaker
```

### 第一个游戏：移动的角色

```python
from Walimaker import *

# 初始化游戏窗口
setup(800, 600)
title("我的第一个Walimaker游戏")

# 创建角色（size 决定刚体形状；只给图片则不会创建刚体）
character = Character(["idle_1.png", "idle_2.png", "idle_3.png"], size=(32, 32))
character.scale(2, 2)

# 想让它受重力就加一行（默认零重力，y 轴向上所以取负值）
set_gravity(0, -1200)

# 主游戏循环
while True:
    # 事件处理：key_pressed 接收 pygame 的按键常量
    # （K_UP / K_a / K_SPACE 等常量随 `from Walimaker import *` 一起提供）
    if key_pressed(K_UP):
        character.forward(5)
    if key_pressed(K_LEFT):
        character.left(5)
    if key_pressed(K_RIGHT):
        character.right(5)
    if key_pressed(K_SPACE):
        character.say("跳跃！")

    # 动画播放
    character.play_anim()
    
    # 渲染更新
    update()
```

## 🧭 中文报错提示（教学特性）

程序出错时，**英文原始报错会完整保留**（异常类型、消息、完整 traceback），中文解释追加在它的后面，
方便初学者对照着官方报错一起看：

```
Traceback (most recent call last):
  File "main.py", line 12, in <module>
    if key_pressed(K_SPACE):
NameError: name 'K_SPACE' is not defined

────────────────────────────────────────────────────────────────
【Walimaker 中文提示】按键/事件常量「K_SPACE」没有导入
  原因：pygame 的按键、鼠标、事件常量（K_SPACE、KEYDOWN、QUIT、MOUSEBUTTONDOWN 等）需要先导入才能使用。
  解决：① 确认 walimaker 版本 ≥ 2.1.0（pip install -U walimaker），这些常量会随 from Walimaker import *
        一起提供；② 或者显式写 from pygame.locals import *。
  参考：CHANGELOG 2.1.0「恢复 pygame 事件/按键常量的导出」
────────────────────────────────────────────────────────────────
```

覆盖的常见错误包括：缺少依赖、变量/函数未定义（按键常量有专项提示）、忘记调用 `setup()`、
图片/音乐路径找不到、没有声卡、pymunk 坐标类型错误、pymunk 版本兼容问题、`key_pressed("w")` 传了字符串、
图片类型不支持、`setup()` 参数不对、Tab 与空格混用、语法/缩进错误、除零、全局变量作用域、递归过深等；
如果报错发生在 Walimaker 自身代码里，会提示"可能是框架问题 + 如何反馈"。

**关闭方式**（默认开启）：

```bash
WALIMAKER_ERROR_HELP=0 python main.py     # 环境变量
```

```python
set_error_help(False)                     # 或在代码里关闭
```

**在 try/except 里主动取中文解释**：

```python
try:
    character = Character("hero.png")
except Exception as e:
    print(explain_exception(e))           # 返回中文解释文本
```

## 📁 项目结构

```
Walimaker/
├── 📄 pyproject.toml        # 打包 / 依赖配置
├── 📄 README.md             # 项目说明
├── 📄 LICENSE               # MIT 许可证
├── 📄 OPTIMIZATION_PLAN.md  # 性能 / 结构分析与优化计划
├── 📁 benchmarks/           # 性能基线脚本（不参与打包）
│   └── 🐍 perf_baseline.py
├── 📁 examples/             # 示例代码：物理引擎、约束、瓦片地图（含示例 TMX 关卡）
│   ├── 🐍 physics_basics.py
│   ├── 🐍 physics_constraints.py
│   ├── 🐍 tiledmap_basics.py
│   └── 📁 assets/           # level.tmx + tiles.png
├── 📁 tests/                # 冒烟 / 回归测试（不参与打包）
│   └── 🐍 smoke_test.py
└── 📁 src/
    └── 📁 Walimaker/        # 包源码（import Walimaker）
        ├── 📄 API.py             # 主要API接口（Character、窗口管理等）
        ├── 📄 sprite.py          # 精灵和动画系统
        ├── ⚙️ physics.py         # 物理引擎封装（基于Pymunk）
        ├── 📷 camera.py          # 相机系统（缩放、跟随）
        ├── 🗺️ tiledmap.py        # TileMap地图系统
        ├── 📝 textbox.py         # UI文本组件
        ├── 🎵 music.py           # 音频管理系统
        ├── 📦 resource_manager.py # 资源管理（智能缓存）
        ├── 🔧 config.py          # 全局配置和初始化
        ├── 🎮 test.py            # 示例代码（walimaker-demo 入口）
        └── 📁 static/            # 资源文件（只有体积很小的图片）
            └── 🖼️ *.png          # 图片资源
```

> **关于字体**：从 2.3.0 起包内**不再携带字体文件**（原先的楷体/宋体合计 28.6 MB，
> 使 wheel 接近 16 MB，且属于微软版权字体不允许再分发）。中文显示改为运行时自动解析，
> 详见下文「🅰️ 中文字体」一节。

开发时可用 `pip install -e .`（或把 `src/` 加入 `PYTHONPATH`）后 `import Walimaker`。

### 🧰 本地开发与测试（uv，推荐）

项目已按 [uv](https://docs.astral.sh/uv/) 配好开发环境：**一条命令**建好隔离环境并装齐依赖
（`[dependency-groups] dev`：black / flake8 / mypy / pytest / twine）。

```bash
uv sync                                  # 创建 .venv（Python 版本由 .python-version 指定）并安装依赖
uv run python scripts/check.py           # 静态检查 + 全部测试（等价于 CI）
uv run python scripts/check.py --matrix  # 额外跑 pymunk 6.11.1 / 最新版 双版本矩阵
uv run python tests/smoke_test.py        # 也可以只跑单个测试
uv run python benchmarks/perf_baseline.py
```

- `uv.lock` 随仓库提供（133 个包，按 Python 3.8.1+ 全范围解析），`uv sync` 按锁文件精确安装；
  升级依赖用 `uv lock --upgrade`。
- `--matrix` 用 `uv run --no-project --with pymunk==6.11.1` 现开临时环境，两个版本各跑一遍全部测试，
  **不需要手动准备两个 venv**、也不会污染当前环境（整轮约 15 秒）。
- 不想用 uv 也可以：`pip install -e ".[dev]"` 后 `python scripts/check.py`，效果相同。

**Python 支持矩阵**（`requires-python = ">=3.8.1"`）：

| 版本 | 说明 |
|---|---|
| 3.8.1 ~ 3.8.x | 可用，但会被解析到 **pymunk 6.x**（pymunk 7 要求 ≥ 3.9）；3.8.0 不受支持（没有任何 flake8 版本可用） |
| 3.9 ~ 3.13 | 推荐区间：pymunk 7 + 官方 `pygame` wheel 齐备 |
| 3.14+ | 官方 `pygame` 尚无 wheel，需改用 [`pygame-ce`](https://pypi.org/project/pygame-ce/) |

## 📚 核心模块详解

| 模块 | 职责 | 主要入口 |
|---|---|---|
| `config.py` | 世界状态、初始化、坐标换算、更新容器 | `World` / `global_var` / `resolve_world` / `init` / `to_cp` |
| `API.py` | 公共 API 门面 + 场景级工具函数 | `setup` / `update` / `bgpic` / `preload` / `tracer` … |
| `objects.py` | 对象模型（组合下面三个 mixin） | `NewGameObject` / `Character` / `Sensor` / `Wall` / `Mouse` |
| `sprite.py` | 精灵与五种动画策略 | `Sprite` / `SpriteSheet` / `EasySpriteStrategy` / `ListSpriteStrategy` / `AnimatorStrategy` / `TiledMapStrategy` |
| `animation.py` `collision.py` `input.py` | 对象模型的动画 / 碰撞 / 鼠标交互（mixin） | 由 `NewGameObject` 组合，一般不直接用 |
| `physics.py` | 刚体、刚体组、瓦片刚体、物理子步 | `Body` / `BodiesGroup` / `TiledMapBodies` / `TiledMapBodiesGroup` |
| `constraints.py` | 约束（关节） | `connect` / `Connect` / `Spring` |
| `camera.py` | 相机：跟随、缩放、抖动 | `Camera`（每个世界一个：`world.CAMERA`） |
| `tiledmap.py` | TMX 地图解析、对象层 | `TiledMap` |
| `screen.py` | 窗口、主循环、固定物理步与子步 | `setup` / `update` / `done` / `save_screen` / `title` / `Screen` |
| `textbox.py` | 文本框与对话框 | `TextBox` / `DialogBox` |
| `events.py` | 鼠标 / 键盘 / 文本输入 | `key_pressed` / `get_mouse_pos` … |
| `music.py` | 背景音乐与音效 | `bgmusic` / `music_*` / `play_snd` |
| `resource_manager.py` `lru_cache.py` | 资源缓存（图片/字体/音效 LRU；mask 弱引用） | `ResourceManager` / `LRUCache` / `preload` |
| `error_help.py` | 中文报错助手 | `explain_exception` / `set_error_help` … |
| `fonts.py` | 字体解析链（包内 → 系统 → pygame 默认） | `font_info` / `resolve_font` |
| `facilitate.py` `draw.py` `draw_options.py` | 小工具：符号函数、线段绘制、物理调试绘制 | `sign` / `draw_line` / `NewDrawOptions` |

```python
from Walimaker import *

# —— 最小可运行骨架 ——
setup(800, 600)              # 建窗口、相机、精灵组、物理空间
title("我的游戏")

hero = Character("hero.png", size=(32, 32))   # 传 size 才有刚体
hero.goto(0, 0)
set_gravity(0, -1200)                          # 默认零重力，需要自己开

while True:
    update()                 # 推进一帧：物理 → 逻辑 → 渲染
```

---

## 📖 API 参考

> 全部 **86 个框架名字** + 558 个 pygame 事件/按键常量都随 `from Walimaker import *` 提供。
> 下面按模块给出用法；标 `world=` 的接口都可以省略该参数（省略即默认世界 `global_var`）。

### 🧭 概述与坐标系

Walimaker 使用**笛卡尔坐标系**，**世界原点 (0, 0) 在窗口中心**，y 轴向上、x 轴向右；
角度 0° 指向右侧、逆时针为正。窗口坐标与世界坐标用下面两个函数互转：

```python
pygame2Cartesian(x, y)     # 窗口坐标 -> 世界坐标（(x-W/2, H/2-y)）
Cartesian2pygame(x, y)     # 世界坐标 -> 窗口坐标
to_cp(pos)                 # 把 Vector2/序列转成 pymunk 能接受的 (x, y) 元组（框架内部用）
random_pos()               # 在世界范围内随机取一个坐标
sign(n)                    # n 的符号：1 / -1 / 0
```

### 🖼️ 窗口与主循环

| 函数 | 说明 | 参数 | 返回值 |
|------|------|------|--------|
| `setup(width, height, world=None)` | 初始化一个世界：窗口、相机、精灵组、物理空间 | 宽、高（整数像素） | `Screen` |
| `update(world=None)` | 推进一帧（物理 → 逻辑 → 渲染） | - | - |
| `done(world=None)` | 进入死循环，直到关窗 | - | - |
| `title(text)` | 设置窗口标题 | 文本 | - |
| `save_screen(path, world=None)` | 把下一帧画面保存为图片 | 路径 | - |
| `init()` | 初始化 pygame / mixer / font（幂等，一般由 `setup()` 调用） | - | - |
| `Screen(size, world=None)` | 屏幕对象（`world.SCREEN`），有 `update()` / `draw()` | - | - |
| `cwd` | 包目录（`Walimaker/static` 的父目录） | - | `str` |
| `audio_available` | 音频设备是否可用（无声卡时为 `False`，不影响图形） | - | `bool` |
| `__version__` | 版本号，例如 `"3.1.0"` | - | `str` |

### 🎮 Character / 对象模型

构造（三种贴图形式，`size` 决定是否创建刚体）：

| 方式 | 例子 | 说明 |
|------|------|------|
| 单图 | `Character("hero.png", size=(32, 32))` | `size=(w, h)` 矩形刚体；`size=16` 半径 16 的圆 |
| 帧动画 | `Character(["a.png", "b.png"], size=(32, 32))` | 列表 = 逐帧播放 |
| 状态动画 | `Character({"idle": [...], "walk": [...]}, size=(32, 32))` | 字典 = 状态机 |
| 瓦片地图 | `Character(tiledmap, size=objs)` | 用地图对象层当碰撞体 |

> ⚠️ **要参与物理（重力 / 碰撞 / 速度），必须传 `size`**：只给图片不会创建刚体，
> `velocity` / `apply_force()` / `mass` 等都不会生效（框架会给一次中文提示）；
> 另外 `setup()` 之后默认重力是 `(0, 0)`，记得 `set_gravity(0, -1200)`。

同族类：`Sensor(imgs, size)`（穿透型触发器）、`Wall(imgs, size)`（STATIC，不动）、
`Mouse(imgs)`（跟随鼠标）、`NewGameObject(...)`（自定义 `body_type` 的基类）。
底层载体是 `GameObject`（`obj._gameObject`）：它持有 sprite、刚体、瓦片刚体与坐标，
`NewGameObject` 只是它的易用封装；需要直接操作精灵/刚体时可以从这里进去。

| 属性 | 类型 | 说明 | 权限 |
|------|------|------|------|
| `x`, `y`, `pos` | `float` / `vec` | 世界坐标 | 读写 |
| `dir` | `vec` | 朝向向量（默认 (1, 0)） | 读写 |
| `rot` | `float` | 旋转角度（度） | 读写 |
| `velocity` | `vec` | 线速度（无刚体时无效） | 读写 |
| `angular_velocity` | `float` | 角速度 | 读写 |
| `mass` / `elasticity` / `friction` | `float` | 质量 / 弹性(0~1) / 摩擦(0~1) | 读写 |
| `red` `green` `blue` `alpha` / `color` | `int` / `tuple` | 着色与透明度 | 读写 |
| `visible` | `bool` | 是否显示与参与渲染 | 读写（`show()` / `hide()`） |
| `width`, `height` | `int` | 贴图尺寸 | 只读 |
| `layer` | `int` | 渲染层级 | 读写（`move_to_front()` / `move_to_back()`） |
| `frame` | `int` | 当前动画帧 | 读写 |
| `state` | `str` | 当前动画状态（**下一帧生效**） | 读写 |
| `dt` | `float` | 动画帧间隔（仅图片列表策略） | 读写 |
| `name` / `properties` / `tmx_object` | `str` / `dict` / 对象 | TileMap 对象层附加信息 | 读写 |

| 方法 | 说明 | 参数 |
|------|------|------|
| `forward(d)` / `backward(d)` | 沿朝向前进/后退 d 像素（受 `speed()` 限速） | 像素 |
| `left(a)` / `right(a)` | 相对朝向左/右转 a 度 | 角度 |
| `goto(x, y)` / `goto((x, y))` | 瞬移到坐标 | 坐标 |
| `slide_to(pos, v=1)` | 朝目标匀速滑动 | 坐标、速度 |
| `face_to(x, y)` | 朝向某个坐标 | 坐标 |
| `scale(sx, sy=None)` / `body_scale(sx, sy)` | 缩放显示 / 缩放刚体尺寸 | 倍数 |
| `flipx(bool)` / `flipy(bool)` | 水平/垂直翻转 | 布尔 |
| `apply_force(x, y)` | 施加**冲量**（框架沿用"力"的叫法） | 向量 |
| `collide(target)` | 碰撞检测：目标可以是坐标或另一个对象 | 坐标/对象 → `bool` |
| `separate(target)` | "刚刚分开"的那一帧 | 坐标/对象 → `bool` |
| `distance(target)` | 到目标的距离 | 坐标/对象 → `float` |
| `say(text, textColor=(0,0,0))` | 弹出对话框（`say()` 无参关闭） | 文本 |
| `play_snd(path, volume=None)` / `set_volume(v)` | 播放音效 / 设置音量 | 路径、0~1 |
| `play_anim()` / `stop_anim()` | 播放 / 暂停动画 | - |
| `set_dt(state, dt)` | 状态动画的帧间隔（`state=None` 表示全部状态） | 状态名、秒 |
| `set_next_state(from, to)` | 播完 `from` 自动切到 `to` | 状态名 |
| `set_start_func(state, fn)` / `set_end_func(state, fn)` | 进入状态 / 播完一轮的回调 | 状态名、函数 |
| `get_mouse_clicked(b="all")` | 鼠标是否按在该对象上 | 按键号 |
| `get_mouse_just_clicked(b="all")` | 刚刚按下 | 按键号 |
| `get_mouse_just_released(b="all")` | 刚刚松开 | 按键号 |
| `get_mouse_upon()` | 鼠标是否悬停在贴图上 | - |
| `kill()` | 销毁对象（连同刚体、对话框） | - |

### 🧱 物理引擎

```python
set_gravity(0, -1200, world=None)     # 设置重力（默认 (0, 0)）
debug(world=None)                     # 打开物理调试绘制（画出刚体形状）
```

| 名字 | 说明 |
|------|------|
| `Body(body_type, shape, size, sensor, world=None)` | 单个刚体。`body_type`：`"STATIC"` / `"KINEMATIC"` / `"DYNAMIC"`；`shape`：`"CIRCLE"`(size=半径) / `"BOX"`(size=(w,h)) / `"POLY"`(size=顶点列表) |
| `BodiesGroup()` | 刚体更新组（每帧同步显示坐标）。世界里的实例：`world.BODIES` |
| `TiledMapBodies(body_type, objs, sensor, world=None)` | 由一组对象（各自带 `.points`）生成的瓦片刚体集合 |
| `TiledMapBodiesGroup()` | 瓦片刚体更新组：`world.TMBG` |
| `world.SPACE` | 底层 `pymunk.Space`（要自己 `step()`/`add()` 时用） |

**防隧穿（高速物体穿墙）**：物理是固定步长 1/60 秒推进的，一步位移超过碰撞体厚度就会穿过去。
框架按"最快物体"自动切分物理子步：

```python
global_var.MAX_STEP_DISTANCE = 8.0   # 每个子步允许的最大位移（像素）；0 = 关闭子步
global_var.MAX_SUBSTEPS = 16         # 子步数上限
```

### 🔗 约束（关节）

| 名字 | 说明 |
|------|------|
| `connect(a, b, world=None)` | 刚性杆（`pymunk.PinJoint`），保持创建时的距离；返回该关节 |
| `Connect(a, b, world=None)` | 同上（类形式） |
| `Spring(a, b, rest_length=None, stiffness=100, damping=10, anchor_a=(0,0), anchor_b=(0,0), world=None)` | 弹簧-阻尼；`rest_length=None` 取当前距离，之后可直接改 `spring.rest_length` |

`a` / `b` 可以是游戏对象，也可以是物理层的 `Body`。

### 📷 Camera 相机

每个世界一个相机：`global_var.CAMERA` 或 `World().CAMERA`。

| 成员 | 说明 |
|------|------|
| `follow(subject)` | 跟随某个对象（`subject` 需要有 `pos`） |
| `deadzone = (半宽, 半高)` | 跟随死区：主体在死区内移动时相机不动（默认 `(0, 0)` 严格居中） |
| `scale(s)` / `zoom_to(s, speed=2.0)` | 直接设置缩放 / 平滑缩放到 s |
| `smooth_zoom = True` | 缩放时用 `pygame.transform.smoothscale`（更平滑、稍慢） |
| `shake(amount=8, duration=0.3)` / `stop_shake()` | 屏幕抖动 / 立即停止 |
| `x`, `y`, `pos`, `offset`, `size`, `scl` | 位置、贴到窗口的偏移、尺寸、缩放 |
| `surface` | 相机画面（`update()` 后可直接 `pygame.image.save`） |

### 🗺️ TiledMap 瓦片地图

| 成员 | 说明 |
|------|------|
| `TiledMap("map.tmx")` | 解析 TMX（会读图集，需要 `setup()` 之后再创建） |
| `tiled.width` / `tiled.height` | 地图像素尺寸 |
| `bgpic(tiled)` | 把地图铺成背景并渲染（相机尺寸自动 = 地图大小），返回背景对象 |
| `tiled.render(surface)` / `tiled.make_map()` | 把瓦片层画到指定 surface / 生成整张地图 surface |
| `tiled.objects` / `tiled.get_objects(name=None)` | 对象层的解析结果（`obj.name/type/x/y/points/properties`；坐标已换算） |
| `tiled.create_objects(name=None, body_type="STATIC", sensor=False, images=None, world=None)` | 把对象层实例化为游戏对象：矩形/多边形变 POLY 刚体，TMX 图块对象自动带贴图 |

### 🎨 Sprite 精灵（更底层的接口）

> 平时用 `Character` 就够了；需要自定义绘制/组合时可以直接操作精灵。

| 名字 | 说明 |
|------|------|
| `Sprite(imgs, world=None)` | 精灵：`imgs` 可以是 `Surface` / 路径 / 列表 / 状态字典 / `SpriteSheet` / `TiledMap` |
| `Sprite.autos` | 类型 → 策略的映射表（可自行注册新策略） |
| `EasySpriteStrategy(img)` | 单帧策略 |
| `ListSpriteStrategy(imgs)` | 帧动画策略（`frame` / `set_dt` / `playing`） |
| `AnimatorStrategy(imgs_dict)` | 状态动画策略（`state` / `set_dt(state, dt)` / `set_next_state` / `set_start_func` / `set_end_func`） |
| `TiledMapStrategy(tiledmap)` | 把整张 TMX 当作一帧 |
| `SpriteSheet(sheet, w, h)` | 把一张图按 w×h 网格切成多帧（`sheet[i]`） |
| `sprite.is_flip_h` / `is_flip_v` / `red` / `green` / `blue` / `alpha` / `visible` | 视觉状态 |
| `sprite.image_changed` / `moved` | 本帧是否变化（渲染优化用） |

### 📝 TextBox / DialogBox

| 名字 | 说明 |
|------|------|
| `TextBox(size, font=None, bgfill=None, font_color=(0,0,0), world=None)` | 屏幕文字；`font=None` 自动选字体（见下文「🅰️ 中文字体」） |
| `text.write(content)` / `text.print(content)` | 设置文本（`print` 是兼容别名） |
| `text.pos` / `text.goto(x, y)` / `text.color` | 位置与颜色 |
| `DialogBox(parent, img, font, world=None)` | 跟随对象的对话框（一般通过 `obj.say()` 使用） |

### 🖱️⌨️ 输入（events）

| 函数 | 说明 | 返回值 |
|------|------|--------|
| `get_mouse_pos(world=None)` | 鼠标位置（世界坐标，同一帧内只查询一次） | `tuple` |
| `get_mouse_rel(world=None)` | 本帧鼠标移动量 | `vec` |
| `get_mouse_clicked(b="all", world=None)` | 鼠标按住 | `bool` |
| `get_mouse_just_clicked(b="all", world=None)` | 鼠标刚按下 | `bool` |
| `get_mouse_just_released(b="all", world=None)` | 鼠标刚松开 | `bool` |
| `set_mouse_visible(visible)` | 显示/隐藏光标 | - |
| `key_pressed(key=None, world=None)` | 按住某键；**不传参 = 是否有任意键按下** | `bool` |
| `key_just_pressed(*keys, world=None)` | 某键刚按下；传多个键 = 是否都刚按下；不传参返回本帧按下的键列表 | `bool` / `list` |
| `key_just_released(*keys, world=None)` | 某键刚松开（同上） | `bool` / `list` |
| `key_input(world=None)` | 本帧输入的文本（`TEXTINPUT` 事件） | `str` |

按键常量（`K_UP`、`K_a`、`K_SPACE`、`KEYDOWN`、`QUIT` …）都随 `from Walimaker import *` 提供。

### 🎵 音频（music）

| 函数 | 说明 |
|------|------|
| `bgmusic(path, loops=-1, start=0.0)` | 播放背景音乐（默认循环） |
| `set_volume(v)` | 背景音乐音量（0.0~1.0） |
| `music_load(path)` / `music_queue(path)` | 加载 / 排队下一首 |
| `music_play(loops=-1, start=0.0)` / `music_stop()` / `music_pause()` / `music_unpause()` / `music_rewind()` | 播放控制 |
| `music_fadeout(ms=1000)` | 淡出 |
| `music_set_volume(v)` / `music_get_volume()` / `music_get_busy()` | 音量与状态 |
| `music_set_pos(seconds)` / `music_get_pos()` | 播放位置 |
| `music_set_endevent(event_id=None)` / `music_get_endevent()` | 播放结束时投递的事件（`None` = 取消） |

音效：`obj.play_snd(path, volume=None)`（每个对象最多同时保留 20 路、
`obj.set_volume(v)` 设置默认音量）；`world` 里的 `audio_available` 可判断有没有声卡。

### 📦 资源管理

| 名字 | 说明 |
|------|------|
| `preload(assets, world=None)` | 批量预加载：`{"images": [...], "fonts": [(None, 24)], "sounds": [...]}` |
| `load_image(path, world=None)` | 加载图片（带缓存） |
| `ResourceManager(max_cache_size=1000)` | 资源管理器（单例）；`world.RESOURCE_MANAGER` |
| `.load_image(path)` / `.load_font(path, size)` / `.load_sound(path)` | 单资源加载（LRU 缓存） |
| `.get_mask(surface, threshold=0)` | 像素 mask（**按 Surface 弱引用缓存**） |
| `.preload(assets)` / `.unload(path)` / `.clear_cache(kind=None)` / `.get_cache_info()` | 预加载 / 卸载 / 清缓存 / 看统计 |
| `LRUCache(maxsize)` | 通用 LRU 缓存（`get` / `put` / `clear` / `info`） |

### 🧭 中文报错助手（error_help）

| 名字 | 说明 |
|------|------|
| `explain_exception(exc)` | 在 `try/except` 里主动获取中文解释（英文原文保留） |
| `explain_exception` 的开关：`set_error_help(True/False)` / `is_error_help_enabled()` | 运行时开关 |
| `install_error_help()` / `uninstall_error_help()` | 手动安装 / 卸载 `sys.excepthook`（导入包时已自动安装） |

环境变量 `WALIMAKER_ERROR_HELP=0` 可整体关闭。

### 🅰️ 字体解析（Walimaker.fonts）

| 名字 | 说明 |
|------|------|
| `font_info(path=None)` | 打印当前字体来源与 `has_cjk`，排查"中文显示成方框" |
| `resolve_font(path=None)` / `resolve_font_path(path=None)` | 解析字体（显式路径 → 环境变量 → 包内 → 系统 → pygame 默认） |
| `reset_font_cache()` | 清理字体解析缓存 |

环境变量 `WALIMAKER_FONT` 可指定字体文件（机房统一字体很方便）。

### 🌍 世界与全局状态（config）

| 名字 | 说明 |
|------|------|
| `global_var` | **默认世界**（`World` 实例），所有 `world=` 参数的缺省值 |
| `World()` | 新建一个独立世界（自己的窗口状态、相机、精灵组、物理空间） |
| `resolve_world(world=None)` | 把 `None` 解析成默认世界（框架内部统一用它） |
| `world.WIDTH` / `HEIGHT` | 窗口尺寸 |
| `world.ALL_SPRITES` / `GROUP` / `BODIES` / `TMBG` | 精灵组 / 对象更新组 / 刚体组 / 瓦片刚体组 |
| `world.SPACE` / `CAMERA` / `SCREEN` / `RESOURCE_MANAGER` | 物理空间 / 相机 / 屏幕 / 资源管理器 |
| `world.EVENTS` / `FRAME` / `LINES` / `FORCE_REDRAW` / `DEBUG_DRAW` | 本帧事件、帧号、待绘线段、重绘标记、调试绘制 |
| `world.SPEED` / `IS_UPDATED` | `speed()` 限速、`tracer()` 自动刷新 |
| `world.MAX_STEP_DISTANCE` / `MAX_SUBSTEPS` | 防隧穿子步参数 |
| `world.set_value(name, value)` / `get_value(name)` | 按名字读写世界属性 |
| `Group(update_hook=None)` | 按帧更新的容器（`world.GROUP` 用对象协议，刚体组用钩子） |

### 🛠 场景级工具

| 名字 | 说明 |
|------|------|
| `tracer(enable, world=None)` | 打开后每次 `update()` 结束再自动刷新一次（适合演示/白板） |
| `speed(s, world=None)` | 设置 `forward()` / `left()` 等阻塞式移动的每步位移上限 |
| `debug(world=None)` | 打开物理调试绘制 |
| `draw_line(p1, p2, color, width, world=None)` | 在下一帧的叠加层画一条线 |
| `set_depth(i=1)` | 返回 `"../" * i`（**不再改变工作目录**，需要时自己 `os.chdir`） |
| `Color` / `Rect` | pygame 的类型（随之导出，方便直接用） |


---

## 🚦 高级用法

### 🌍 多世界（互不干扰的两套场景）

每个 `World` 拥有自己的窗口状态、相机、精灵组、物理空间与更新组。
**所有接口的 `world` 参数都可以省略，省略即默认世界**（`global_var`），所以旧代码完全不用改。

```python
from Walimaker import *
from Walimaker.config import World

# 默认世界（老写法，等价于 world=None）
setup(800, 600)
hero = Character("hero.png", size=(32, 32))

# 第二个世界：自己的相机 / 空间 / 精灵组
w2 = World()
setup(800, 600, world=w2)
ghost = Character("ghost.png", size=(32, 32), world=w2)
text = TextBox(24, world=w2)

update()          # 只推进默认世界
update(w2)        # 只推进 w2；两个世界的物理与画面互不影响

ghost.velocity = vec(50, 0)   # 只影响 w2 里的刚体
hero.kill()                   # 只移除默认世界里的对象
```

- 可用 `world` 的接口：`setup / update / done / save_screen`、`Screen`、`Camera`、
  `Character / Sensor / Wall / Mouse / NewGameObject`、`TextBox / DialogBox`、`Sprite`、
  `Body / TiledMapBodies`、`draw_line / set_gravity / debug / tracer / speed / random_pos / bgpic`、
  鼠标与键盘查询函数。
- 资源缓存（`ResourceManager`）是**进程内共享**的：图片/字体只解码一次，两个世界共用。
- 相机不再全局唯一：`World().CAMERA` 属于各自的世界，`Camera((宽, 高))` 会新建实例。

### 📷 相机手感（抖动 / 跟随死区 / 平滑缩放）

```python
from Walimaker import *

hero = Character("hero.png", size=(32, 32))
camera = global_var.CAMERA

camera.follow(hero)                     # 跟随角色
camera.deadzone = (40, 30)              # 在 80x60 的死区内移动时相机不动（默认 0 = 严格居中）
camera.zoom_to(2.0, speed=1.5)          # 平滑缩放到 2 倍（每秒变化 1.5）
camera.smooth_zoom = True               # 缩放时用 smoothscale（更平滑，但比 scale 慢）
camera.shake(amount=8, duration=0.3)    # 被打中 / 爆炸时抖一下
```

这些都默认关闭（`deadzone=(0,0)`、`smooth_zoom=False`、不抖动），因此不影响既有项目。

### ⚡ 高速物体穿墙怎么办（防隧穿）

物理是固定步长（1/60 秒）推进的，而 pymunk/Chipmunk **没有连续碰撞检测（CCD）**：
一步的位移超过碰撞体厚度时，物体会直接穿过去（实测 10px 厚的墙在 **1200 px/s** 时就会穿模）。

框架默认按“最快的物体”自动切分物理子步，保证每个子步的位移不超过 8 像素：

```python
global_var.MAX_STEP_DISTANCE = 8.0   # 每个子步允许的最大位移（像素）；0 = 关闭子步
global_var.MAX_SUBSTEPS = 16         # 子步数上限，防止极端速度把一帧切成几百步
# 自定义世界：w = World(); w.MAX_STEP_DISTANCE = 4.0
```

- **慢速场景子步数恒为 1**，行为与性能都不变（默认 8px 对应约 480 px/s 以下）；
- 超过 `MAX_SUBSTEPS × MAX_STEP_DISTANCE × 60`（默认约 7680 px/s）仍可能穿模——
  这类极端速度请调大上限，或把碰撞体做厚一些（薄墙 + 极高速是最容易穿的组合）。

### 🔗 把两个物体连起来（约束）

```python
from Walimaker import *

a = Character("a.png", size=(32, 32))
b = Character("b.png", size=(32, 32))
a.goto(-50, 0)
b.goto(50, 0)

connect(a, b)                                  # 刚性杆：保持当前距离（pymunk.PinJoint）
Spring(a, b, rest_length=100, stiffness=200)   # 弹簧：把距离拉回 rest_length（DampedSpring）
```

两者都接受游戏对象或物理层的 `Body`，并自动把关节加到对象所属世界的空间；
距离/劲度/阻尼可以直接读改（`spring.rest_length = 80`）。

### 🗺️ TileMap：铺成背景 / 取出对象层

```python
from Walimaker import *

tiled = TiledMap("map.tmx")

# 1) 把整张地图当背景铺上（相机尺寸自动设成地图大小），返回背景对象
bg = bgpic(tiled)

# 2) 把对象层里的对象变成可碰撞的游戏对象（每个对象一个 POLY 刚体）
#    TMX 图块对象会自动带上贴图；也可以用 images={"对象名": 图片} 指定
objects = tiled.create_objects()                    # 全部
spawns = tiled.create_objects(name="spawn")         # 按名字过滤
for obj in objects:
    print(obj.name, obj.properties, tuple(obj.pos))

# 想自己建对象也行：直接看解析结果（几何 + 属性）
for obj in tiled.get_objects():
    print(obj.name, obj.x, obj.y, obj.points)
```

### 状态机与动画控制
```python
from Walimaker import *

character = Character({
    "idle": ["idle_1.png", "idle_2.png"],
    "walk": ["walk_1.png", "walk_2.png", "walk_3.png"],
    "jump": ["jump_1.png", "jump_2.png"]
})

# 设置动画参数
character.set_dt("walk", 0.1)  # 走路动画每帧0.1秒
character.set_next_state("walk", "idle")  # 走路结束后回到空闲状态

# 状态切换回调
def on_jump_start():
    print("开始跳跃!")
    
def on_jump_end():
    print("跳跃结束!")
    character.state = "idle"

character.set_start_func("jump", on_jump_start)
character.set_end_func("jump", on_jump_end)

# 切换状态
character.state = "walk"  # 开始走路
character.state = "jump"  # 开始跳跃（触发回调）
```

### 资源管理
```python
from Walimaker import *
from Walimaker.config import global_var

# 预加载资源（顶层 API，避免第一次使用时卡顿）
preload({
    "images": ["player.png", "enemy.png", "background.png"],
    "fonts": [(None, 24), (None, 36)],          # None = 自动选择中文字体
    "sounds": ["jump.wav", "collect.wav"]
})

# 查看缓存状态
cache_info = global_var.RESOURCE_MANAGER.get_cache_info()
print(f"已缓存图片: {cache_info['images']['current_size']}张")
print(f"已缓存字体: {cache_info['fonts']['current_size']}种")

# 清理缓存
global_var.RESOURCE_MANAGER.clear_cache("image")  # 仅清理图片缓存
```

> 图片/字体/音效用 LRU 缓存；**碰撞 mask 用弱引用缓存**——同一个 Surface 只构建一次，
> Surface 被回收后缓存条目自动消失，不会把临时 Surface 一直留在内存里。

### 🅰️ 中文字体

从 2.3.0 起包内不再携带字体文件（原先的 `simkai.ttf` + `simsun.ttc` 合计 28.6 MB，
使 wheel 接近 16 MB，且属于微软授权字体不允许再分发）。`TextBox` / `DialogBox`
的字体改为**运行时按顺序解析**：

| 顺序 | 来源 | 说明 |
| --- | --- | --- |
| 1 | 显式传入路径 | `TextBox(25, font=r"C:\Windows\Fonts\simhei.ttf")` |
| 2 | 环境变量 `WALIMAKER_FONT` | 机房统一字体：`set WALIMAKER_FONT=D:\fonts\SourceHanSansSC.otf` |
| 3 | 包内 `Walimaker/static/` | 把任意 ttf/ttc/otf 丢进去即可，无需改代码 |
| 4 | 操作系统中文字体 | Windows 微软雅黑/黑体/宋体；macOS 苹方/冬青黑体；Linux Noto CJK/文泉驿 |
| 5 | pygame 默认字体 | 任何平台都存在，但**不含中文字形**（中文显示成方框） |

- **找不到字体不会报错**：只发出一次中文 + 英文的 `RuntimeWarning`，程序继续运行；
  仅当“显式指定的字体文件确实存在但已损坏”时才抛出错误。
- 排查中文显示成方框：

  ```python
  from Walimaker.fonts import font_info
  print(font_info())
  # {'path': '/System/Library/Fonts/Hiragino Sans GB.ttc', 'source': 'system',
  #  'source_label': '操作系统中文字体', 'has_cjk': True}
  ```

  若 `has_cjk` 为 `False`，说明系统里没有中文字体（常见于精简 Linux / Docker 镜像），
  安装 `fonts-noto-cjk`（Debian/Ubuntu：`apt install fonts-noto-cjk`）或设置
  `WALIMAKER_FONT` 指向一个中文字体文件即可。只跑逻辑、不显示画面
  （`SDL_VIDEODRIVER=dummy`）的场景不受影响。

## 🍳 常见任务速查（Cookbook）

```python
from Walimaker import *

setup(800, 600)
```

**① 显示一张图片 / 精灵**

```python
bgpic("bg.png")                              # 背景（也可以是 Surface 或 TiledMap）
hero = Character("hero.png", size=(32, 32))  # size 决定刚体形状
hero.goto(0, -100)
```

**② 按键控制移动（带限速）**

```python
speed(6)                       # forward/left/right 的每步位移上限
while True:
    if key_pressed(K_LEFT):  hero.left(4)
    if key_pressed(K_RIGHT): hero.right(4)
    if key_pressed(K_UP):    hero.forward(6)
    update()
```

**③ 让角色受重力并站到地面上**

```python
set_gravity(0, -1200)
ground = Wall(size=(700, 40)); ground.goto(0, -250)
hero = Character("hero.png", size=(32, 32)); hero.goto(0, 0)
while True:
    if hero.collide(ground):                  # 是否接触（也可传坐标）
        if key_pressed(K_SPACE):
            hero.velocity = (0, 620)          # 向上跳（y 轴向上）
    update()
```

**④ 碰撞时做事（"刚刚撞上" / "刚刚分开"）**

```python
if hero.collide(coin):          # 一直重叠时会持续为 True
    coin.kill(); score += 1
if hero.separate(ground):       # 只在离开地面那一帧为 True
    print("起跳！")
```

**⑤ 显示与更新文字（HUD）**

```python
hud = TextBox(24)
hud.write(f"分数 {score}")
hud.goto(-350, 250)             # 世界坐标
```

**⑥ 播放音乐与音效**

```python
bgmusic("bgm.ogg")              # 循环背景音乐
set_volume(0.6)
hero.play_snd("jump.wav")       # 每个对象最多同时 20 路
```

**⑦ 截图 / 保存某一帧**

```python
save_screen("shot.png")         # 下一帧 update() 时保存
update()
```

**⑧ 计时与节奏（不依赖真实时间）**

```python
if global_var.FRAME % 60 == 0:  # 约每秒一次（帧率上限 60）
    spawn_enemy()
```

**⑨ 多世界（互不干扰的两套场景）**

```python
w2 = World()
setup(800, 600, world=w2)
ghost = Character("ghost.png", size=(32, 32), world=w2)
update()          # 默认世界
update(w2)        # 第二个世界
```

**⑩ 瓦片地图三件事**

```python
tiled = TiledMap("level.tmx")
bgpic(tiled)                                     # 铺成背景
solids = tiled.create_objects(name="ground_body")  # 对象层 -> 碰撞体
coins  = tiled.create_objects(name="coin_a")       # 图块对象自动带贴图
spawn  = tiled.get_objects("spawn")[0]             # 取出生点坐标
```

**⑪ 绳索 / 弹簧**

```python
connect(anchor, box)                                  # 刚性杆
spring = Spring(anchor, ball, rest_length=100)        # 弹簧
spring.rest_length = 140                              # 随时改自然长度
```

**⑫ 相机跟随 + 受击抖动**

```python
cam = global_var.CAMERA
cam.follow(hero)
cam.deadzone = (40, 30)
cam.shake(amount=8, duration=0.3)     # 被打中时抖一下
```

---

## 📊 性能优化建议

### 最佳实践
1. **资源复用**：尽量复用Character对象，避免频繁创建销毁
2. **动画优化**：使用精灵表和状态机，减少Draw Call
3. **物理优化**：将静态物体设为STATIC类型，减少物理计算
4. **内存管理**：使用resource_manager的预加载功能
5. **批处理**：相似的对象尽量使用相同的材质和大小

### 常见性能问题
- **问题**: 游戏卡顿，FPS下降
- **检查**: 使用`debug()`模式查看物理调试绘制
- **解决**: 减少同时活动的物理对象数量

- **问题**: 内存占用过高
- **检查**: 查看resource_manager缓存状态
- **解决**: 调整缓存大小或手动清理缓存

## ❓ 常见问题（FAQ）

**Q：我用 `Character("hero.png")` 创建角色，为什么它不掉下来、`velocity` 也没反应？**
A：两件事要同时满足：① 创建时**传 `size`**（`Character("hero.png", size=(32, 32))`），
只给图片不会创建刚体；② 调 `set_gravity(0, -1200)`——默认是零重力。
漏掉时框架会给一次中文提示。

**Q：`AttributeError: 'NoneType' object has no attribute ...`？**
A：多半是还没 `setup(宽, 高)`（窗口、相机、精灵组、物理空间都由它创建）；
中文报错助手会直接提示。

**Q：中文显示成方框？**
A：说明当前字体没有中文字形。先 `from Walimaker.fonts import font_info; print(font_info())`：
`has_cjk=False` 时请安装系统 CJK 字体（Linux：`apt install fonts-noto-cjk`）、
设置环境变量 `WALIMAKER_FONT`，或把字体文件放进 `Walimaker/static/`。

**Q：子弹/被击飞的箱子会穿过墙？**
A：物理每步位移超过碰撞体厚度就会"隧穿"。框架已按最快物体自动切分物理子步
（默认每个子步 ≤8 像素）；极高速时调小 `global_var.MAX_STEP_DISTANCE` 或调大
`global_var.MAX_SUBSTEPS`，也可以把墙做厚一点。

**Q：报错说找不到图片/音乐？**
A：相对路径是相对于**运行脚本时所在的目录**（不是代码文件所在目录），且区分大小写；
建议用 `os.path.join(os.path.dirname(__file__), ...)` 拼绝对路径。

**Q：按键判断失效 / `key_pressed("w")` 报错？**
A：`key_pressed` 要传 pygame 的按键常量：`key_pressed(K_w)`（常量随 `from Walimaker import *` 提供），
传字符串会抛 `TypeError`。不传参数表示"是否有任意键按下"。

**Q：怎么在服务器/CI 上跑（没有显示器）？**
A：需要一次 `setup()`（会创建窗口）；无显示器环境用 SDL 的 dummy 驱动即可：
`SDL_VIDEODRIVER=dummy SDL_AUDIODRIVER=dummy python your_game.py`
（本仓库的 `tests/` 与 `examples/` 都是这样跑的）。

**Q：报错能不能给中文解释？**
A：默认已开启：英文原始 traceback 完整保留，其后追加中文的「原因 / 解决 / 参考」。
也可在 `try/except` 里用 `explain_exception(e)` 主动取解释；`WALIMAKER_ERROR_HELP=0` 关闭。

**Q：为什么安装包很小、里面没有字体？**
A：2.3.0 起不再打包字体（原先的楷体/宋体合计 28.6 MB 且属微软版权字体，不能随包分发），
中文改为运行时解析系统字体，详见「🅰️ 中文字体」。

---

## 🧪 示例代码

**`examples/` 目录**里是可直接运行的完整示例（重点演示物理引擎与瓦片地图）：

```bash
uv run python examples/physics_basics.py        # 重力/三种刚体/碰撞/冲量/材质
uv run python examples/physics_constraints.py   # 刚性链 connect() + 弹簧 Spring()
uv run python examples/tiledmap_basics.py       # TMX 地图当背景 + 对象层变碰撞体/金币
```

它们都支持 `WALIMAKER_EXAMPLE_FRAMES=N`（跑 N 帧后退出），方便无显示器自测；
详细说明见 `examples/README.md`。

另外 `Walimaker/test.py`（`walimaker-demo` 入口）里还有一个完整的内置 demo：
平台跳跃、弹球物理、TileMap、UI 交互。

## 🤝 贡献指南

欢迎贡献代码！请遵循以下步骤：

1. **Fork项目**
2. **创建功能分支** (`git checkout -b feature/AmazingFeature`)
3. **提交修改** (`git commit -m 'Add some AmazingFeature'`)
4. **推送分支** (`git push origin feature/AmazingFeature`)
5. **开启Pull Request**

### 代码规范
- 遵循 **PEP 8** 编码规范
- 使用 **类型注解** (Python 3.7+)
- **注释**：重要功能和复杂逻辑需要注释
- **测试**：新增功能需包含测试用例

## 📄 许可证

本项目采用 **MIT 许可证** - 查看 [LICENSE](LICENSE) 文件了解详情。

## 📞 支持与反馈

- **文档问题 / Bug 报告**：附上重现步骤、错误日志与运行环境（Python、pygame、pymunk 版本）
- **功能建议**：欢迎直接反馈
- **版本与更新日志**：见 [PyPI 项目页](https://pypi.org/project/walimaker/) 与仓库根目录的 `CHANGELOG.md`

## 🙏 致谢

感谢以下开源项目：
- [Pygame](https://www.pygame.org/) - 游戏开发库
- [Pymunk](https://www.pymunk.org/) - 2D物理引擎
- [Pytmx](https://github.com/pytmx/pytmx) - TMX地图解析库

## 🚀 下一步计划（Roadmap）

> **诚信说明**：早期版本的介绍里曾把下面几项列为「已实现特性」，实际尚未落地。
> 现在统一移到 Roadmap，未做就是未做；欢迎反馈你最想要哪一个。

- [ ] 代码热重载（改代码即时生效，无需重启）
- [ ] 可视化编辑器
- [ ] 插件 / 组件系统
- [ ] 常用 UI 控件（按钮、输入框、列表…）
- [ ] 触控 / 移动端手势
- [ ] WebAssembly 支持（让 Walimaker 在浏览器中运行）
- [ ] 3D 渲染扩展（基于 OpenGL）
- [ ] 网络多人游戏支持

---

## 🧭 坐标系说明
- **坐标系统**：笛卡尔坐标系，**世界原点 (0, 0) 在窗口中心**，x 向右、**y 向上**
  （所以 `goto(0, 200)` 在窗口上半部分；向上跳要给正的 y 速度）
- **窗口坐标换算**：`pygame2Cartesian(x, y)` / `Cartesian2pygame(x, y)`；鼠标与键盘事件给的是窗口坐标
- **角度系统**：0°指向右侧，逆时针方向角度增加
- **颜色值范围**：RGB 通道 0-255，透明度 0-255
- **音量控制**：0.0（静音）到 1.0（最大音量）
- **物理单位**：像素；**默认重力是 `(0, 0)`（零重力）**，需要自己 `set_gravity(0, -1200)`
  （负值才向下，因为 y 轴向上；教学示例常用 900～1400）

> **教学提示**：Walimaker的设计目标是让编程初学者能够在30分钟内制作出第一个可玩的游戏。从简单到复杂，循序渐进地学习游戏开发的核心概念。
