AGENTS.md · diff
git:20260922.c9ec6ed to git:20260927.f6a97ad
89 added, 150 removed. Audit A to A.
- # Project Directory Structure
-
- Separate **framework**, **resources (by type)**, and **game code**. Do not mix them.
-
- ```
- project-root/
- ├── zfoo/ # Framework only — sync/upgrade; no game business logic
- ├── assets/ # Extra assets (video, fonts, 3D, …)
- ├── audio/ # Audio assets
- ├── image/ # Image assets
- ├── config/ # CSV/JSON tables (optional)
- ├── shader/ # Custom shaders (optional)
- ├── scene/ # Runnable and instanced .tscn scenes
- ├── script/ # Game scripts (.gd), systems, data models
- ├── test/ # Unit tests (UnitTest.gd scenes)
- └── project.godot
- ```
-
- ## Resource directories (no `.gd` logic)
-
- **`image/`** — textures and sprites:
-
- ```
- image/
- ├── ui/ # Buttons, panels, icons
- ├── characters/ # Character sprites and portraits
- ├── backgrounds/ # Scene backgrounds
- ├── tiles/ # Tilemap tiles
- └── effects/ # VFX and particle textures
- ```
-
- **`audio/`** — use with zfoo `Audio` API:
-
- ```
- audio/
- ├── bgm/ # Looping background music
- ├── sfx/ # Short one-shot sound effects
- └── voice/ # Voice-over and narration
- ```
-
- **`assets/`** — video, fonts, 3D, and other extra binary assets:
-
- ```
- assets/
- ├── video/ # Cutscenes, trailers, background video
- ├── font/ # Font files (.ttf, .otf, …)
- ├── 3d/ # Models, meshes, materials, animations
- └── … # Other misc assets as needed
- ```
-
- **Other resource roots** (add when needed): `config/`, `shader/`.
-
- ## Game code — `scene/` and `script/`
-
- ```
- scene/
- ├── boot/
- ├── main/
- ├── gameplay/
- └── ui/
-
- script/
- ├── autoload/ # Game Autoloads (after GodotFramework)
- ├── core/ # Constants, ResPath, shared base classes
- ├── data/ # Resource classes and .tres instances
- ├── systems/ # Pure logic (inventory, save, quest, …)
- └── network/ # Packets and codec (zfoo Router)
- ```
+ # GDScript Rules
- # GDScript (this project)
+ ## General
- **Types**: Use explicit types on function signatures and return values (`-> void`, etc.). Use the `class_name` type when a class has one. **Prefer `:=` for locals** to lock in the inferred type at declaration; use `var x = ...` only when you need Variant or mixed types.
- **Docs**: Use `##` comments for scene entry points or complex logic. Match the tone of nearby files.
- **Nodes**: Prefer `@onready var name: Type = $Path`.
- - **Compact formatting**: Keep code on one line whenever possible.
+ - **Compact formatting**: Keep code on one line when it remains readable.
- **Trailing pass**: If a function has no `return` statement, end the body with `pass`.
- # Underscores and naming (Godot 4)
+ ## Naming
**`_` is mainly for engine callbacks** (`_ready`, `_process`, `_notification`, `_init`, etc.). **Do not prefix business methods** like `_refresh_xxx` — that clutters the file with `_` like lifecycle hooks. Use `_` on variables sparingly for internal details. GDScript has no real private; use structure and folders instead.
**Member variables**: Normal state/refs usually **no `_`** (`player`, `news_cache`). Too many `_` names hurt autocomplete and search, and look like lifecycle hooks. Use `_` only for clear implementation details (`_http_client`, `_buffer`, `_is_loading`). Official small demos often use `var _speed`; large projects do not need that pattern everywhere.
| Kind | Naming | Notes |
|------|--------|-------|
- | Engine lifecycle | `_ready`, `_process`, `_input`, `physics_*`, etc. | Keep the official `_` prefix. |
+ | Engine lifecycle | `_ready`, `_process`, `_physics_process`, `_input`, etc. | Keep the official `_` prefix. |
| Normal members | `player`, `ui_panel`, `news_cache` | **No** `_` prefix. |
| Internal vars | `_http_client`, `_buffer`, `_retry_count` | Implementation detail; **use sparingly**. |
| Signal handlers | `on_buy_pressed`, `on_timer_timeout`, or `handle_buy`, `handle_close` | **No** `_on_*`; keep separate from engine hooks. |
- | Business / utils | `refresh_trendings`, `set_tab`, `load_config_file` | **No** `_` prefix; distinguish from lifecycle funcs. |
+ | Business / utils | `refresh_trending_items`, `set_tab`, `load_config_file` | **No** `_` prefix; distinguish from lifecycle funcs. |
When connecting signals in `_ready`, prefer:
```gdscript
func _ready() -> void:
button.pressed.connect(on_buy_item)
pass
func on_buy_item() -> void:
pass
func refresh_ui() -> void:
update_labels()
pass
```
- # godot-framework
+ ---
- ## AI — OpenAI-compatible chat
- ```gdscript
- var client := OpenAiClient.new(OS.get_environment("OPENAI_API_KEY"), "https://api.deepseek.com/chat/completions", "deepseek-v4-flash")
- var reply := await client.async_chat("hello", "you are a helpful assistant")
+ # Godot Framework Reference
- # Streaming — on_delta called for each token fragment; read full text from completion.content
- var stream_completion := await client.async_chat_messages_stream(
- client.build_messages("hello", "you are a helpful assistant"),
- [],
- "",
- func(delta: String, stream_kind: String): print(delta)
- )
- var streamed := stream_completion.content
+ ## GodotFramework — gdf
- # Multi-turn
- var messages: Array[ChatMessage] = []
- messages.append(ChatMessage.new(ChatMessage.ROLE_USER, "hello"))
- var reply2 := await client.async_chat_messages(messages)
+ The Autoload node runs `GodotFramework.gd`, whose global class name is `gdf`.
- # Tool-calling agent loop (see agent/)
- var tools: Array[OpenAiToolDef] = registry.get_schemas()
- var completion := await client.async_chat_messages_stream(messages, tools, "", func(delta: String, stream_kind: String): print(delta))
- ```
+ ```gdscript
+ # Defer a callable to the main thread (useful from network / worker callbacks)
+ gdf.callable_deferred(func() -> void: refresh_ui())
- ---
+ # Graceful exit (waits a few frames before quit)
+ await gdf.quit()
+ ```
- ## Alert — Floating toast messages
+ ## AI — OpenAI-compatible chat
```gdscript
- Alert.alert("Saved successfully", Colors.success)
- Alert.alert("Network error", Colors.error)
- ```
+ var client := OpenAiClient.new(OS.get_environment("OPENAI_API_KEY"), "https://api.deepseek.com/chat/completions", "deepseek-v4-flash")
+ var reply := await client.async_chat("hello", "you are a helpful assistant")
- ---
+ # Streaming — on_delta called for each token fragment; read full text from completion.content
+ var stream_completion := await client.async_chat_messages_stream(client.build_messages("hello", "you are a helpful assistant"), tools,
+ proxy, func(delta: String, stream_kind: String): print(delta))
+ var streamed := stream_completion.content
+ ```
- ## Audio — play music, sound, voice,SoundEffect
+ ## Audio — music, voice, and sound effects
```gdscript
# Single track or playlist (auto cross-fade near end of track)
Audio.play_music("res://audio/bgm.mp3")
Audio.play_musics(["res://audio/a.mp3", "res://audio/b.mp3"])
# One-shot sound / voice
await Audio.play_voice("res://audio/narration.mp3")
# Multi-channel SFX (overlapping sounds on SoundEffect bus)
Audios.play("res://audio/click.mp3", 0.8)
```
- ---
## Animation
```gdscript
- # plays a one-shot sprite sheet animation and removes itself when finished. Multi-row sheet: 4 columns × 4 rows, scale 0.5, 13 fps
+ # Plays a one-shot sprite sheet animation and removes itself when finished. Multi-row sheet: 4 columns × 4 rows, scale 0.5, 13 fps
EffectAnimation2D.spawn(Vector2(500, 200), self, "res://effects/attack.png", Vector2i(4, 4), 0.5, 13)
```
- ---
## Unit tests
- Attach `zfoo/gdtest/UnitTest.gd` to a scene; it scans `.gd` files in the scene’s folder.
- In each file, every no-arg method whose name **starts or ends with `test`** (case-insensitive) is run as a unit test.
## Integration tests
- Attach `zfoo/gdtest/IntegrationTest.gd` to a scene; it scans the scene’s folder for `.tscn` whose name **starts or ends with `test`**, then runs them one by one.
- Each finished test scene must emit `gdf.events.test_passed` (UnitTest does this automatically).
- ---
- ## HotUpdate
+ ## Hot update
- - Godot PCK Hot Update for single pck
+ - Godot PCK hot update for a single PCK
- Workflow: Launch App → Check Version → Download PCK → Verify MD5 → Load PCK → Enter Game
- ---
- ## Http
+ ## HTTP
```gdscript
# GET request
var response := await HttpHelper.async_get("https://api.example.com/data")
if response.success:
- Log.info(response.get_body_string())
+ Log.info(response.get_body_string())
```
- ---
## Log
- - file logger at `{user_data}/logs/godot.log`
+ - Logs are written to `{user_data}/logs/godot.log`.
```gdscript
Log.info("player login uid:[{}]", user_id)
Log.error("load failed path:[{}] err:[{}]", path, err)
```
- ---
## Network
- - support `TcpClient`, `TcpClientThread`, `WebsocketClient`, `WebsocketClientThread`
+ - Supports `TcpClient`, `TcpClientThread`, `WebsocketClient`, and `WebsocketClientThread`.
```gdscript
- # Create a network seesion
+ # Create a network session
# `ICodec` for encode/decode
var session: Session = TcpClient.new(Codec.new(), "127.0.0.1:80")
# Register receiver (typically at login / session init)
Router.register_receiver(LoginResponse, func(packet: LoginResponse) -> void: on_login_response(packet))
- # Send message is Fire-and-forget
+ # Send a message without waiting for a response (fire-and-forget)
Router.send(session, SomeRequest.new())
# Request–response (waits for matching reply or timeout)
var reply: LoginResponse = await Router.async_ask(session, LoginRequest.new())
```
- ---
## ResourceHelper — async loading
- Avoid blocking the main thread when loading large assets.
```gdscript
var texture: Texture2D = await ResourceHelper.async_load("res://assets/icon.svg")
var scene: PackedScene = await ResourceHelper.async_load("res://scene/Level.tscn")
```
- ---
## SceneHelper — scenes & nodes
```gdscript
# Switch scene with fade transition (default: RectTransitionFade)
await SceneHelper.async_change_scene_to_file("res://scene/Main.tscn")
# Custom slide transition
await SceneHelper.async_change_scene_to_file("res://scene/Main.tscn", RectTransitionSlide.new())
- # Instantiate a scene as child of a node
+ # Instantiate a scene as a child of a node
var node := SceneHelper.add_scene_to_node(load("res://scene/Popup.tscn"), self)
# Safe queue_free
SceneHelper.queue_free(old_node)
```
- ---
## SchedulerBus — delayed & periodic tasks
```gdscript
var sw := StopWatch.new() # sw.cost_seconds()
# Run once after 1000 ms
SchedulerBus.schedule(func() -> void: do_something(), 1000)
# Run every 2000 ms (optional timer name, optional sub-thread)
SchedulerBus.schedule_at_fixed_rate(func() -> void: poll_status(), 2000)
```
- ---
## Setting — persistent user config
```gdscript
Setting.set_bool("sound_enabled", true)
Setting.set_string("nickname", "player1")
Setting.save()
var enabled := Setting.get_bool("sound_enabled", false)
var name := Setting.get_string("nickname", "")
```
- ---
- ## Utils — common helpers
+ ## I18n — localization
- - Also available: `ArrayUtils`, `CollectionUtils`, `NumberUtils`, `NetUtils`, `HttpUtils`, `IdUtils`, `RateLimitUtils`.
+ Use `I18n.t()` to translate text:
```gdscript
- # StringUtils
- var msg := StringUtils.format("score:[{}] name:[{}]", score, name)
- if StringUtils.is_blank(text):
- return
+ label.text = I18n.t("settings.title")
+ ```
- # TimeUtils
- var ts := TimeUtils.now() # cached ms timestamp (updated each second)
- var now_str := TimeUtils.date() # "yyyy-mm-dd hh:mm:ss"
- # JsonUtils — plain objects with public fields
- var obj = JsonUtils.json_to_object('{"name":"test","age":10}', Student)
- var json := JsonUtils.object_to_json(obj)
+ ## Collection — collection utilities
- # FileUtils
- FileUtils.write_string_to_file("user://log.txt", content)
- var text := FileUtils.read_file_to_string("user://log.txt")
- FileUtils.delete_file_or_directory("user://log.txt")
+ `ConcurrentArrayList`, `ConcurrentMapInt`, `LazyCache`, `LruStringCache`, `ReadyQueue`, `RingIntList`, `RingStringList`
- # RandomUtils
- var n := RandomUtils.random_int_limit(100)
- var item = RandomUtils.random_ele(items)
- # ThreadUtils — non-blocking wait on main thread
- await ThreadUtils.async_sleep(500)
- ```
+ ## Common — common utilities
+ `StringBuilder`, `Utf8StreamDecoder`
+
+
+ ## Utils — common helpers
+
+ - `ArrayUtils`, `CollectionUtils`, `FileUtils`, `GitUtils`, `GlobUtils`
+ - `HttpUtils`, `IdUtils`, `JsonUtils`, `MarkdownUtils`, `NetUtils`
+ - `NodeUtils`, `NumberUtils`, `OSUtils`, `ProxyUtils`, `RandomUtils`
+ - `RateLimitUtils`, `ReflectionUtils`, `StringUtils`, `ThreadUtils`, `TimeUtils`
+
+
---
- ## GodotFramework — gdf
- The Autoload node runs `GodotFramework.gd`, whose global class name is `gdf`.
+ # UI Style Guide
+ ## Typography
+
+ - `Fonts`: Lazily loads the bundled Noto Sans SC fonts in light, regular, medium, semibold, and bold weights.
+ - `Typography`: Provides shared display, headline, title, body, and label sizes, plus letter-spacing values.
+
+
+ ## Layout and controls
+
+ - `Margin`: Provides spacing tokens in 4-pixel increments.
+ - `ControlSize`: Provides standard control heights, corner radii, border widths, and square sizes.
+ - `StyleBoxHelper`: Creates `StyleBoxFlat` instances and applies their content margins.
+ - `ButtonStyle`: Builds and applies theme-aware button state boxes and font colors.
+ - `ScrollBarStyle`: Applies standard or custom scrollbar thickness, colors, and rounding.
+
+
+ ## Theme and colors
+
+ - `ThemeColor`: Manages dark/light mode, the user accent color, and colors derived from the active theme.
+ - `ColorBase`: Provides theme-aware semantic colors for surfaces, text, borders, and status feedback.
+ - `ColorFile`: Provides theme-aware colors for audio, image, video, text, and folder types.
+ - `ColorMarkdown`: Provides theme-aware colors used by Markdown rendering.
+
+ Avoid hard-coded fonts, sizes, spacing, radii, and colors when a matching shared token exists.
+
+
+ ## Feedback — alerts, desktop toasts, popup windows
+
```gdscript
- # Defer a callable to the main thread (useful from network / worker callbacks)
- gdf.callable_deferred(func() -> void: refresh_ui())
+ # Brief in-app feedback: show a non-blocking top-center message with a semantic result color.
+ Alert.alert("Saved successfully", ColorBase.success)
+ Alert.alert("Network error", ColorBase.error)
- # Graceful exit (waits a few frames before quit)
- await gdf.quit()
+ # Background-task feedback: show an OS-level notification when the app may be unfocused.
+ DesktopToast.show_toast("Run finished", "All tasks completed", ColorBase.success)
+
+ # Detailed feedback: show important or long content in a popup sized by viewport percentages.
+ PopupWindow.show_window("Details", "Full feedback message", 70, 80)
```