feellab-mcp 0.1.0__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.
- feellab_mcp-0.1.0/.gitignore +4 -0
- feellab_mcp-0.1.0/CHANGELOG.md +16 -0
- feellab_mcp-0.1.0/LICENSE +21 -0
- feellab_mcp-0.1.0/PKG-INFO +127 -0
- feellab_mcp-0.1.0/PUBLISHING.md +18 -0
- feellab_mcp-0.1.0/README.md +108 -0
- feellab_mcp-0.1.0/docs/TIMELINE.md +30 -0
- feellab_mcp-0.1.0/examples/demo_timelines.py +12 -0
- feellab_mcp-0.1.0/pyproject.toml +33 -0
- feellab_mcp-0.1.0/server.json +28 -0
- feellab_mcp-0.1.0/src/feellab/__init__.py +4 -0
- feellab_mcp-0.1.0/src/feellab/__main__.py +46 -0
- feellab_mcp-0.1.0/src/feellab/config.py +12 -0
- feellab_mcp-0.1.0/src/feellab/data/checklist.json +783 -0
- feellab_mcp-0.1.0/src/feellab/data/scenes.json +71 -0
- feellab_mcp-0.1.0/src/feellab/demo.py +96 -0
- feellab_mcp-0.1.0/src/feellab/evaluate.py +102 -0
- feellab_mcp-0.1.0/src/feellab/measure.py +220 -0
- feellab_mcp-0.1.0/src/feellab/metrics.py +101 -0
- feellab_mcp-0.1.0/src/feellab/server.py +82 -0
- feellab_mcp-0.1.0/src/feellab/timeline.py +89 -0
- feellab_mcp-0.1.0/tests/test_feellab.py +102 -0
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## 0.1.0
|
|
4
|
+
|
|
5
|
+
The first release, built for ToangTown (2026-10-15).
|
|
6
|
+
|
|
7
|
+
* **The checklist:** 82 interactions of a first-person shooter in ten groups (shooting, hitting, getting hit,
|
|
8
|
+
movement, the world, enemies, explosions, the sound mix, the screen, smoothness), each with what the player should
|
|
9
|
+
get; 23 measured from a timeline, the rest left for a person with what to look for.
|
|
10
|
+
* **The timeline contract** (`feellab-timeline/1`, docs/TIMELINE.md): the camera every frame and every event (input,
|
|
11
|
+
shot, sound, effect, decal, impact, marker, damage dealt and taken, camera, screen, debug) with its time.
|
|
12
|
+
* **26 metrics** over 11 standard scenes: latency, sound-to-picture sync, kick and its recovery, shake and its fall
|
|
13
|
+
with distance, presence of every answer, footstep cadence, head bob, frame hitches. Each rule names its source or
|
|
14
|
+
is marked an estimate.
|
|
15
|
+
* **Verdicts, a report and gate rows** in Weapon Lab's row format.
|
|
16
|
+
* **An example:** timelines of two made-up games, one with every answer and one bare prototype.
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Khoa Hoang
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: feellab-mcp
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: An MCP server that checks how a first-person shooter feels: an 82-item checklist of every interaction, a timeline contract any game's probe can write, and measures of latency, kick, shake, presence and smoothness.
|
|
5
|
+
Project-URL: Homepage, https://github.com/hpdkhoa/feellab-mcp
|
|
6
|
+
Project-URL: Issues, https://github.com/hpdkhoa/feellab-mcp/issues
|
|
7
|
+
Author: Khoa Hoang
|
|
8
|
+
License-Expression: MIT
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
Keywords: fps,game-development,game-feel,juice,mcp,testing
|
|
11
|
+
Classifier: Development Status :: 3 - Alpha
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Topic :: Games/Entertainment
|
|
15
|
+
Classifier: Topic :: Software Development :: Testing
|
|
16
|
+
Requires-Python: >=3.10
|
|
17
|
+
Requires-Dist: mcp<3,>=2.0
|
|
18
|
+
Description-Content-Type: text/markdown
|
|
19
|
+
|
|
20
|
+
<h1 align="center">Feel Lab</h1>
|
|
21
|
+
|
|
22
|
+
<!-- mcp-name: io.github.hpdkhoa/feellab-mcp -->
|
|
23
|
+
|
|
24
|
+
<p align="center"><strong>An FPS feel checklist, measured: every interaction a player has, checked from what the game really did.</strong></p>
|
|
25
|
+
|
|
26
|
+
<p align="center">
|
|
27
|
+
<a href="LICENSE"><img alt="license: MIT" src="https://img.shields.io/badge/license-MIT-blue"></a>
|
|
28
|
+
<a href="pyproject.toml"><img alt="Python 3.10+" src="https://img.shields.io/badge/Python-3.10%2B-3776ab?logo=python&logoColor=white"></a>
|
|
29
|
+
<a href="https://pypi.org/project/feellab-mcp/"><img alt="PyPI: feellab-mcp" src="https://img.shields.io/pypi/v/feellab-mcp?label=PyPI&logo=pypi&logoColor=white&color=3775a9"></a>
|
|
30
|
+
<a href="https://registry.modelcontextprotocol.io/v0/servers?search=feellab-mcp"><img alt="MCP Registry: feellab-mcp" src="https://img.shields.io/badge/MCP%20Registry-feellab--mcp-6f42c1"></a>
|
|
31
|
+
<a href="#the-checklist"><img alt="checklist: 82 interactions" src="https://img.shields.io/badge/checklist-82%20interactions-orange"></a>
|
|
32
|
+
<a href="#tools"><img alt="tools: 6" src="https://img.shields.io/badge/tools-6-orange"></a>
|
|
33
|
+
<a href="docs/TIMELINE.md"><img alt="timeline contract: v1" src="https://img.shields.io/badge/timeline%20contract-v1-orange"></a>
|
|
34
|
+
</p>
|
|
35
|
+
|
|
36
|
+
Game feel is what a player feels between pressing a key and seeing the world answer. A shot should flash, sound and
|
|
37
|
+
kick on the same frame; a round striking a wall should throw dust, make a sound and leave a hole; a hit taken should
|
|
38
|
+
jerk the view and show where it came from; a blast nearby should shake the screen more than one far away. When one
|
|
39
|
+
answer is missing or late, the game feels floaty or cheap, and nobody can say why.
|
|
40
|
+
|
|
41
|
+
Feel Lab turns that into checks. Your game's probe plays standard scenes and writes a **timeline**: the camera every
|
|
42
|
+
frame, and every sound, effect, mark and damage event with its time. Feel Lab measures latency, kick, shake,
|
|
43
|
+
presence, match and smoothness from it, and judges each item of an 82-item checklist. It never runs your game: any
|
|
44
|
+
engine can write the timeline ([docs/TIMELINE.md](docs/TIMELINE.md)).
|
|
45
|
+
|
|
46
|
+
## The checklist
|
|
47
|
+
|
|
48
|
+
| Group | Items | Measured from a timeline |
|
|
49
|
+
|---|---|---|
|
|
50
|
+
| A. Shooting (your own gun) | 19 | 9 |
|
|
51
|
+
| B. Hitting (your bullets land) | 15 | 6 |
|
|
52
|
+
| C. Getting hit (damage taken) | 9 | 4 |
|
|
53
|
+
| D. Movement and the body | 11 | 2 |
|
|
54
|
+
| E. Interacting with the world | 7 | 0 |
|
|
55
|
+
| F. Enemies (zombies) | 5 | 0 |
|
|
56
|
+
| G. Explosions, fire and the world | 5 | 1 |
|
|
57
|
+
| H. Sound mix | 4 | 0 |
|
|
58
|
+
| I. Screen and comfort | 4 | 0 |
|
|
59
|
+
| J. Smoothness (feel breaks when the frame does) | 3 | 1 |
|
|
60
|
+
|
|
61
|
+
Items no timeline can show (a colour-blind option, a reload cancel's timing) are left for a person, with what to
|
|
62
|
+
look for. `feellab checklist` prints them all.
|
|
63
|
+
|
|
64
|
+
## What it measures
|
|
65
|
+
|
|
66
|
+
| Measure | Example |
|
|
67
|
+
|---|---|
|
|
68
|
+
| Latency | the shot's flash and sound on the shot's frame; the hit marker within 50 ms of the hit |
|
|
69
|
+
| Sync | the shot's sound within 45 ms of its flash (ITU-R BT.1359) |
|
|
70
|
+
| Kick | the camera turns at least 0.3 degrees on a burst and settles within 0.6 s |
|
|
71
|
+
| Shake | a blast 5 m away shakes the view; one 50 m away at least 3 times less |
|
|
72
|
+
| Presence | every impact has an effect, a sound and a mark; every round fired ejects a case |
|
|
73
|
+
| Rhythm | footsteps at walking cadence (about 100 steps a minute and up); the head bobs |
|
|
74
|
+
| Smoothness | no frame twice the median length round a shot or a blast |
|
|
75
|
+
|
|
76
|
+
Every rule names its source. A range from game practice rather than evidence is marked as an estimate.
|
|
77
|
+
|
|
78
|
+
## Try it
|
|
79
|
+
|
|
80
|
+
```bash
|
|
81
|
+
pip install feellab-mcp
|
|
82
|
+
feellab demo demo
|
|
83
|
+
feellab judge demo
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
The demo writes timelines for two made-up games: "Juicy" passes every measured item; "Bare" (a debug dot where a
|
|
87
|
+
round lands, no shake, silent steps) fails 17.
|
|
88
|
+
|
|
89
|
+
## Use it in your game
|
|
90
|
+
|
|
91
|
+
1. Have your probe play the scenes in `src/feellab/data/scenes.json` and write one timeline each
|
|
92
|
+
([docs/TIMELINE.md](docs/TIMELINE.md)). Wrap the calls that start sounds and effects so each is recorded with its
|
|
93
|
+
time; note the moments with no sound or effect of their own (a shot, an impact, a hit marker, damage, a flinch).
|
|
94
|
+
2. Record a camera frame before the scene's cause (a shot, a hit, a blast): kick and flinch are measured from it.
|
|
95
|
+
3. Tag what you can (`"tags": ["muzzle"]`). Where your asset names do not say what a sound or effect is for, put a
|
|
96
|
+
`feel_rules.json` beside the timelines: `{"dry": "slidelock"}` tells Feel Lab that a sound named after a slide lock
|
|
97
|
+
is the click of an empty gun.
|
|
98
|
+
4. `feellab judge <folder>`, or `gate_rows` for a gate.
|
|
99
|
+
|
|
100
|
+
The first game to use it (ToangTown) found 52 failing cases on four guns: a debug dot where a round lands, no shake
|
|
101
|
+
anywhere, no blood, no footsteps, a climb that never comes back, a stall on the first shot.
|
|
102
|
+
|
|
103
|
+
## Tools
|
|
104
|
+
|
|
105
|
+
| Tool | What it does |
|
|
106
|
+
|---|---|
|
|
107
|
+
| `list_checklist` | The checklist by group: what the player should get, measured or for a person. |
|
|
108
|
+
| `list_scenes` | The standard scenes a probe plays. |
|
|
109
|
+
| `list_metrics` | Every metric: scene, unit, rule and source. |
|
|
110
|
+
| `check_timeline` | A timeline file against the contract. |
|
|
111
|
+
| `judge_folder` | A folder of timelines judged: each item's values and verdict, and a Markdown report. |
|
|
112
|
+
| `gate_rows` | The metrics as rows for a gate (Weapon Lab's row format). |
|
|
113
|
+
|
|
114
|
+
```json
|
|
115
|
+
{"mcpServers": {"feellab": {"command": "feellab-mcp"}}}
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
`FEELLAB_OUT` is the only folder Feel Lab writes to (its reports); `~/.feellab` by default.
|
|
119
|
+
|
|
120
|
+
## Where it came from
|
|
121
|
+
|
|
122
|
+
Feel Lab was built for ToangTown, an Unreal 5.8 co-op zombie FPS, next to Weapon Lab
|
|
123
|
+
([weaponlab-mcp](https://github.com/hpdkhoa/weaponlab-mcp)), whose Gate can judge Feel Lab's rows as cases.
|
|
124
|
+
|
|
125
|
+
## License
|
|
126
|
+
|
|
127
|
+
MIT
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# Publishing Feel Lab
|
|
2
|
+
|
|
3
|
+
The MCP Registry accepts the listing only after the PyPI package is live, and checks that the README on PyPI carries
|
|
4
|
+
`mcp-name: io.github.hpdkhoa/feellab-mcp` (README.md holds it as an HTML comment).
|
|
5
|
+
|
|
6
|
+
1. **GitHub:** push `main` to https://github.com/hpdkhoa/feellab-mcp and tag the release (`git tag v0.1.0`,
|
|
7
|
+
`git push --tags`).
|
|
8
|
+
2. **PyPI**, with your token from your password manager (never in a file):
|
|
9
|
+
|
|
10
|
+
python -m pip install build twine
|
|
11
|
+
python -m build
|
|
12
|
+
python -m twine check dist/*
|
|
13
|
+
python -m twine upload dist/*
|
|
14
|
+
|
|
15
|
+
3. **The MCP Registry**, in this folder: `mcp-publisher login github`, then `mcp-publisher publish`.
|
|
16
|
+
|
|
17
|
+
Each release: `python -m pytest tests` (set nothing else needed), raise the
|
|
18
|
+
version in `pyproject.toml`, `src/feellab/__init__.py` and `server.json` (both fields), add it to CHANGELOG.md.
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
<h1 align="center">Feel Lab</h1>
|
|
2
|
+
|
|
3
|
+
<!-- mcp-name: io.github.hpdkhoa/feellab-mcp -->
|
|
4
|
+
|
|
5
|
+
<p align="center"><strong>An FPS feel checklist, measured: every interaction a player has, checked from what the game really did.</strong></p>
|
|
6
|
+
|
|
7
|
+
<p align="center">
|
|
8
|
+
<a href="LICENSE"><img alt="license: MIT" src="https://img.shields.io/badge/license-MIT-blue"></a>
|
|
9
|
+
<a href="pyproject.toml"><img alt="Python 3.10+" src="https://img.shields.io/badge/Python-3.10%2B-3776ab?logo=python&logoColor=white"></a>
|
|
10
|
+
<a href="https://pypi.org/project/feellab-mcp/"><img alt="PyPI: feellab-mcp" src="https://img.shields.io/pypi/v/feellab-mcp?label=PyPI&logo=pypi&logoColor=white&color=3775a9"></a>
|
|
11
|
+
<a href="https://registry.modelcontextprotocol.io/v0/servers?search=feellab-mcp"><img alt="MCP Registry: feellab-mcp" src="https://img.shields.io/badge/MCP%20Registry-feellab--mcp-6f42c1"></a>
|
|
12
|
+
<a href="#the-checklist"><img alt="checklist: 82 interactions" src="https://img.shields.io/badge/checklist-82%20interactions-orange"></a>
|
|
13
|
+
<a href="#tools"><img alt="tools: 6" src="https://img.shields.io/badge/tools-6-orange"></a>
|
|
14
|
+
<a href="docs/TIMELINE.md"><img alt="timeline contract: v1" src="https://img.shields.io/badge/timeline%20contract-v1-orange"></a>
|
|
15
|
+
</p>
|
|
16
|
+
|
|
17
|
+
Game feel is what a player feels between pressing a key and seeing the world answer. A shot should flash, sound and
|
|
18
|
+
kick on the same frame; a round striking a wall should throw dust, make a sound and leave a hole; a hit taken should
|
|
19
|
+
jerk the view and show where it came from; a blast nearby should shake the screen more than one far away. When one
|
|
20
|
+
answer is missing or late, the game feels floaty or cheap, and nobody can say why.
|
|
21
|
+
|
|
22
|
+
Feel Lab turns that into checks. Your game's probe plays standard scenes and writes a **timeline**: the camera every
|
|
23
|
+
frame, and every sound, effect, mark and damage event with its time. Feel Lab measures latency, kick, shake,
|
|
24
|
+
presence, match and smoothness from it, and judges each item of an 82-item checklist. It never runs your game: any
|
|
25
|
+
engine can write the timeline ([docs/TIMELINE.md](docs/TIMELINE.md)).
|
|
26
|
+
|
|
27
|
+
## The checklist
|
|
28
|
+
|
|
29
|
+
| Group | Items | Measured from a timeline |
|
|
30
|
+
|---|---|---|
|
|
31
|
+
| A. Shooting (your own gun) | 19 | 9 |
|
|
32
|
+
| B. Hitting (your bullets land) | 15 | 6 |
|
|
33
|
+
| C. Getting hit (damage taken) | 9 | 4 |
|
|
34
|
+
| D. Movement and the body | 11 | 2 |
|
|
35
|
+
| E. Interacting with the world | 7 | 0 |
|
|
36
|
+
| F. Enemies (zombies) | 5 | 0 |
|
|
37
|
+
| G. Explosions, fire and the world | 5 | 1 |
|
|
38
|
+
| H. Sound mix | 4 | 0 |
|
|
39
|
+
| I. Screen and comfort | 4 | 0 |
|
|
40
|
+
| J. Smoothness (feel breaks when the frame does) | 3 | 1 |
|
|
41
|
+
|
|
42
|
+
Items no timeline can show (a colour-blind option, a reload cancel's timing) are left for a person, with what to
|
|
43
|
+
look for. `feellab checklist` prints them all.
|
|
44
|
+
|
|
45
|
+
## What it measures
|
|
46
|
+
|
|
47
|
+
| Measure | Example |
|
|
48
|
+
|---|---|
|
|
49
|
+
| Latency | the shot's flash and sound on the shot's frame; the hit marker within 50 ms of the hit |
|
|
50
|
+
| Sync | the shot's sound within 45 ms of its flash (ITU-R BT.1359) |
|
|
51
|
+
| Kick | the camera turns at least 0.3 degrees on a burst and settles within 0.6 s |
|
|
52
|
+
| Shake | a blast 5 m away shakes the view; one 50 m away at least 3 times less |
|
|
53
|
+
| Presence | every impact has an effect, a sound and a mark; every round fired ejects a case |
|
|
54
|
+
| Rhythm | footsteps at walking cadence (about 100 steps a minute and up); the head bobs |
|
|
55
|
+
| Smoothness | no frame twice the median length round a shot or a blast |
|
|
56
|
+
|
|
57
|
+
Every rule names its source. A range from game practice rather than evidence is marked as an estimate.
|
|
58
|
+
|
|
59
|
+
## Try it
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
pip install feellab-mcp
|
|
63
|
+
feellab demo demo
|
|
64
|
+
feellab judge demo
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
The demo writes timelines for two made-up games: "Juicy" passes every measured item; "Bare" (a debug dot where a
|
|
68
|
+
round lands, no shake, silent steps) fails 17.
|
|
69
|
+
|
|
70
|
+
## Use it in your game
|
|
71
|
+
|
|
72
|
+
1. Have your probe play the scenes in `src/feellab/data/scenes.json` and write one timeline each
|
|
73
|
+
([docs/TIMELINE.md](docs/TIMELINE.md)). Wrap the calls that start sounds and effects so each is recorded with its
|
|
74
|
+
time; note the moments with no sound or effect of their own (a shot, an impact, a hit marker, damage, a flinch).
|
|
75
|
+
2. Record a camera frame before the scene's cause (a shot, a hit, a blast): kick and flinch are measured from it.
|
|
76
|
+
3. Tag what you can (`"tags": ["muzzle"]`). Where your asset names do not say what a sound or effect is for, put a
|
|
77
|
+
`feel_rules.json` beside the timelines: `{"dry": "slidelock"}` tells Feel Lab that a sound named after a slide lock
|
|
78
|
+
is the click of an empty gun.
|
|
79
|
+
4. `feellab judge <folder>`, or `gate_rows` for a gate.
|
|
80
|
+
|
|
81
|
+
The first game to use it (ToangTown) found 52 failing cases on four guns: a debug dot where a round lands, no shake
|
|
82
|
+
anywhere, no blood, no footsteps, a climb that never comes back, a stall on the first shot.
|
|
83
|
+
|
|
84
|
+
## Tools
|
|
85
|
+
|
|
86
|
+
| Tool | What it does |
|
|
87
|
+
|---|---|
|
|
88
|
+
| `list_checklist` | The checklist by group: what the player should get, measured or for a person. |
|
|
89
|
+
| `list_scenes` | The standard scenes a probe plays. |
|
|
90
|
+
| `list_metrics` | Every metric: scene, unit, rule and source. |
|
|
91
|
+
| `check_timeline` | A timeline file against the contract. |
|
|
92
|
+
| `judge_folder` | A folder of timelines judged: each item's values and verdict, and a Markdown report. |
|
|
93
|
+
| `gate_rows` | The metrics as rows for a gate (Weapon Lab's row format). |
|
|
94
|
+
|
|
95
|
+
```json
|
|
96
|
+
{"mcpServers": {"feellab": {"command": "feellab-mcp"}}}
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
`FEELLAB_OUT` is the only folder Feel Lab writes to (its reports); `~/.feellab` by default.
|
|
100
|
+
|
|
101
|
+
## Where it came from
|
|
102
|
+
|
|
103
|
+
Feel Lab was built for ToangTown, an Unreal 5.8 co-op zombie FPS, next to Weapon Lab
|
|
104
|
+
([weaponlab-mcp](https://github.com/hpdkhoa/weaponlab-mcp)), whose Gate can judge Feel Lab's rows as cases.
|
|
105
|
+
|
|
106
|
+
## License
|
|
107
|
+
|
|
108
|
+
MIT
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# The timeline contract (feellab-timeline/1)
|
|
2
|
+
|
|
3
|
+
measure. A timeline is one JSON file a scene:
|
|
4
|
+
|
|
5
|
+
{"contract": "feellab-timeline/1",
|
|
6
|
+
"scene": "fire_wall", which standard scene this is (scenes.json), or the game's own name
|
|
7
|
+
"subject": "Rifle_M4", the weapon (or whatever the scene is about)
|
|
8
|
+
"params": {"distance_m": 5}, the scene's settings as the probe ran it
|
|
9
|
+
"frames": [{"t": 0.0, "dt": 16.6, seconds from the scene's start; the frame's length in ms
|
|
10
|
+
"cam": [x, y, z, pitch, yaw, roll, fov]}], the player's camera in the world (cm, degrees)
|
|
11
|
+
"events": [{"t": 0.21, "kind": "sound", "name": "sfx_wpn_ar_fire_01", "at": [x, y, z], "value": 1.0, "tags": ["shot"]}]}
|
|
12
|
+
|
|
13
|
+
Event kinds (a probe reports the ones it has; Feel Lab judges what it gets):
|
|
14
|
+
input the probe pressed something (name: fire, reload, jump, ...)
|
|
15
|
+
shot a round left the gun (value: rounds; tags: calibre)
|
|
16
|
+
sound a sound started (name: the asset; value: volume; tags: what it is for, when the game knows)
|
|
17
|
+
fx a visual effect started (name: the asset; tags: muzzle, shell, impact, blood, explosion, tracer, ...)
|
|
18
|
+
decal a mark was left on a surface (tags: the surface)
|
|
19
|
+
impact a round met the world or a body (tags: the surface or "flesh"; value: the damage)
|
|
20
|
+
marker a hit marker was shown (tags: hit, kill, headshot)
|
|
21
|
+
damage_dealt the player's hit landed (value: the damage; tags: headshot, kill)
|
|
22
|
+
damage_taken the player was hurt (value: the damage; at: where the hit came from)
|
|
23
|
+
camera a camera effect started (name: shake, flinch, kick; value: its size)
|
|
24
|
+
ui a screen effect started or changed (name: damage_indicator, blood_overlay, low_health, ...)
|
|
25
|
+
debug something drawn for developers only (a debug point where an impact should be)
|
|
26
|
+
Positions in cm, the world's axes; time in seconds from the scene's start.
|
|
27
|
+
|
|
28
|
+
## Files
|
|
29
|
+
|
|
30
|
+
One file a scene: `<subject>_<scene>_timeline.json`. `feellab check <file>` checks one. `src/feellab/data/scenes.json` lists the standard scenes and what each asks the probe to do.
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
"""Writes example timelines for two made-up games (feellab.demo): python examples/demo_timelines.py <folder>"""
|
|
2
|
+
import sys
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "src"))
|
|
6
|
+
from feellab.demo import game # noqa: E402
|
|
7
|
+
|
|
8
|
+
if __name__ == "__main__":
|
|
9
|
+
out = sys.argv[1] if len(sys.argv) > 1 else "demo_timelines"
|
|
10
|
+
game(out, "Juicy", True)
|
|
11
|
+
game(out, "Bare", False)
|
|
12
|
+
print("written to", out)
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling>=1.24"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "feellab-mcp"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "An MCP server that checks how a first-person shooter feels: an 82-item checklist of every interaction, a timeline contract any game's probe can write, and measures of latency, kick, shake, presence and smoothness."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = "MIT"
|
|
11
|
+
license-files = ["LICENSE"]
|
|
12
|
+
authors = [{ name = "Khoa Hoang" }]
|
|
13
|
+
requires-python = ">=3.10"
|
|
14
|
+
keywords = ["mcp", "game-feel", "fps", "game-development", "testing", "juice"]
|
|
15
|
+
classifiers = [
|
|
16
|
+
"Development Status :: 3 - Alpha",
|
|
17
|
+
"Intended Audience :: Developers",
|
|
18
|
+
"Programming Language :: Python :: 3",
|
|
19
|
+
"Topic :: Games/Entertainment",
|
|
20
|
+
"Topic :: Software Development :: Testing",
|
|
21
|
+
]
|
|
22
|
+
dependencies = ["mcp>=2.0,<3"]
|
|
23
|
+
|
|
24
|
+
[project.urls]
|
|
25
|
+
Homepage = "https://github.com/hpdkhoa/feellab-mcp"
|
|
26
|
+
Issues = "https://github.com/hpdkhoa/feellab-mcp/issues"
|
|
27
|
+
|
|
28
|
+
[project.scripts]
|
|
29
|
+
feellab-mcp = "feellab.server:main"
|
|
30
|
+
feellab = "feellab.__main__:main"
|
|
31
|
+
|
|
32
|
+
[tool.hatch.build.targets.wheel]
|
|
33
|
+
packages = ["src/feellab"]
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://static.modelcontextprotocol.io/schemas/2025-12-11/server.schema.json",
|
|
3
|
+
"name": "io.github.hpdkhoa/feellab-mcp",
|
|
4
|
+
"title": "Feel Lab",
|
|
5
|
+
"description": "An FPS feel checklist, measured from a timeline any game's probe can write",
|
|
6
|
+
"version": "0.1.0",
|
|
7
|
+
"repository": {
|
|
8
|
+
"url": "https://github.com/hpdkhoa/feellab-mcp",
|
|
9
|
+
"source": "github"
|
|
10
|
+
},
|
|
11
|
+
"packages": [
|
|
12
|
+
{
|
|
13
|
+
"registryType": "pypi",
|
|
14
|
+
"identifier": "feellab-mcp",
|
|
15
|
+
"version": "0.1.0",
|
|
16
|
+
"transport": {
|
|
17
|
+
"type": "stdio"
|
|
18
|
+
},
|
|
19
|
+
"environmentVariables": [
|
|
20
|
+
{
|
|
21
|
+
"name": "FEELLAB_OUT",
|
|
22
|
+
"description": "The only folder Feel Lab writes to (its reports); defaults to ~/.feellab",
|
|
23
|
+
"isRequired": false
|
|
24
|
+
}
|
|
25
|
+
]
|
|
26
|
+
}
|
|
27
|
+
]
|
|
28
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
"""Feel Lab: an FPS feel checklist, measured. Every interaction a player has with a first-person shooter (a shot, a
|
|
2
|
+
hit, a hit taken, a step) and what they should get from it; a timeline contract any game's probe can write; measures
|
|
3
|
+
of latency, kick, shake, presence, match and smoothness from it; verdicts and rows a gate can judge."""
|
|
4
|
+
__version__ = "0.1.0"
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
"""feellab <command>
|
|
2
|
+
serve the MCP server (also: feellab-mcp)
|
|
3
|
+
checklist [group] the checklist
|
|
4
|
+
judge <folder> [subject] judge a folder of timelines (prints the report)
|
|
5
|
+
check <timeline.json> a timeline against the contract
|
|
6
|
+
demo <folder> example timelines of two made-up games"""
|
|
7
|
+
import json
|
|
8
|
+
import sys
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def main(argv=None):
|
|
12
|
+
a = list(sys.argv[1:] if argv is None else argv)
|
|
13
|
+
if not a or a[0] in ("-h", "--help"):
|
|
14
|
+
print(__doc__)
|
|
15
|
+
return 0
|
|
16
|
+
if a[0] == "serve":
|
|
17
|
+
from .server import main as serve
|
|
18
|
+
serve()
|
|
19
|
+
return 0
|
|
20
|
+
from . import evaluate as E, timeline as T
|
|
21
|
+
if a[0] == "checklist":
|
|
22
|
+
c = E.checklist()
|
|
23
|
+
for i in c["items"]:
|
|
24
|
+
if len(a) < 2 or i["group"] == a[1]:
|
|
25
|
+
print("%-4s %-8s %s: %s" % (i["id"], i["check"], i["name"], i["expect"]))
|
|
26
|
+
return 0
|
|
27
|
+
if a[0] == "judge":
|
|
28
|
+
for s, tls in E.load_folder(a[1], a[2] if len(a) > 2 else None).items():
|
|
29
|
+
print(E.report_md(s, E.judge(tls)))
|
|
30
|
+
return 0
|
|
31
|
+
if a[0] == "demo":
|
|
32
|
+
from .demo import game
|
|
33
|
+
game(a[1], "Juicy", True)
|
|
34
|
+
game(a[1], "Bare", False)
|
|
35
|
+
print("written to", a[1])
|
|
36
|
+
return 0
|
|
37
|
+
if a[0] == "check":
|
|
38
|
+
from pathlib import Path
|
|
39
|
+
bad = T.check(json.loads(Path(a[1]).read_text(encoding="utf-8-sig")))
|
|
40
|
+
print(json.dumps({"ok": not bad, "problems": bad}, indent=1))
|
|
41
|
+
return 0 if not bad else 1
|
|
42
|
+
raise SystemExit(__doc__)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
if __name__ == "__main__":
|
|
46
|
+
sys.exit(main())
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
"""Where Feel Lab keeps its data (the checklist, the scenes) and writes its reports (FEELLAB_OUT, ~/.feellab)."""
|
|
2
|
+
import os
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
PACKAGE = Path(__file__).resolve().parent
|
|
6
|
+
DATA = PACKAGE / "data"
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def out(*parts):
|
|
10
|
+
p = Path(os.environ.get("FEELLAB_OUT") or Path.home() / ".feellab").joinpath(*parts)
|
|
11
|
+
p.mkdir(parents=True, exist_ok=True)
|
|
12
|
+
return p
|