@genex-ai/cli-demo 0.48.0-dev.80 → 0.48.0-dev.89
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.
- package/package.json +1 -1
- package/templates/skills/genex-ai-hud/SKILL.md +4 -1
- package/templates/skills/genex-threejs-game-ui/SKILL.md +26 -6
- package/templates/skills/genex-threejs-multiplayer/SKILL.md +68 -4
- package/templates/skills/genex-threejs-multiplayer/references/genre-recipes.md +42 -0
- package/templates/skills/genex-threejs-skill-router/references/routing-map.md +6 -4
package/package.json
CHANGED
|
@@ -128,7 +128,10 @@ scaffolding, plain-CSS placeholder bars) while it renders — the CSS HUD keeps
|
|
|
128
128
|
the game playable until the sprites land. When the gate produced a concept
|
|
129
129
|
frame, anchor Stage 1 to it — `--edit <concept-url>` holds the game's
|
|
130
130
|
register far better than a text-only prompt; single-sprite regenerations
|
|
131
|
-
anchor the same way.
|
|
131
|
+
anchor the same way. The concept is a STYLE anchor only: the widget set and
|
|
132
|
+
layout come from the element inventory and the game contract — a mechanic
|
|
133
|
+
the concept invented (a lap counter, a stamina orb) does not enter the HUD,
|
|
134
|
+
and a mechanic it failed to show still does. Generate 2 mockup candidates in one
|
|
132
135
|
call (`--candidates 2`) and pick the better one: a re-roll costs the whole
|
|
133
136
|
serial chain, a second candidate costs nothing extra in wall-clock.
|
|
134
137
|
|
|
@@ -68,12 +68,32 @@ you're borrowing. The bar is what those games ship, not "good enough for a
|
|
|
68
68
|
demo". Conventions only — never logos, exact layouts, or trade dress. The
|
|
69
69
|
same 2–3 names carry into the router's visual-direction gate for the scene.
|
|
70
70
|
Then turn references + brief into ONE **concept frame** — a single generated
|
|
71
|
-
image of
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
to
|
|
71
|
+
image of a PLAYABLE MOMENT of this game, not key art. Build the prompt from
|
|
72
|
+
the game contract, not just the mood: what the player is DOING mid-action
|
|
73
|
+
(the verb), what threatens them right now (enemy silhouettes), what they
|
|
74
|
+
are chasing (the objective — a finish gate, a goal, a pickup), and how the
|
|
75
|
+
space reads (route, scale) — plus the brief's palette, materials, and
|
|
76
|
+
light. A beautiful empty vista with nothing to fight and nowhere to go is a
|
|
77
|
+
FAILED concept; so is one that invents mechanics the game doesn't have (a
|
|
78
|
+
lap counter in a game without laps). `--aspect 16:9 --quality high
|
|
79
|
+
--no-wait`, enqueued FIRST of all art; the URL goes into the style-brief
|
|
80
|
+
comment.
|
|
81
|
+
|
|
82
|
+
**Share it before building on it.** The moment the frame lands, show it to
|
|
83
|
+
the user — "this is roughly how the game will look: keep it, or change
|
|
84
|
+
something?" — as ONE structured question with 2–3 concrete adjustment
|
|
85
|
+
options. Keep building gameplay-neutral work (scaffold, physics, netcode)
|
|
86
|
+
while waiting, but do NOT anchor further art to an unapproved frame; if the
|
|
87
|
+
user redirects, regenerate with their notes before the HUD mockup and menu
|
|
88
|
+
frame go out.
|
|
89
|
+
|
|
90
|
+
**The concept anchors STYLE, not truth.** Palette, materials, light, and
|
|
91
|
+
register come from the frame; CONTENT comes from the game contract. When
|
|
92
|
+
the frame lands, diff it against the contract in one visible line — what
|
|
93
|
+
does this game have that the frame doesn't show (enemies? arenas? the
|
|
94
|
+
finish? the pickup)? Carry those requirements in words into every later
|
|
95
|
+
prompt. Where the frame contradicts gameplay, gameplay wins — never inherit
|
|
96
|
+
an invented mechanic, or the absence of a real one, from a picture.
|
|
77
97
|
|
|
78
98
|
**4. Ask only when genuinely ambiguous.** If the concept pins the mood (a
|
|
79
99
|
"gothic horror dungeon crawler" pins it), decide and state the plan in one
|
|
@@ -99,7 +99,8 @@ owns the queue, roles, winner-stays, forfeit, timeout, and the win condition.
|
|
|
99
99
|
**Choosing a preset — your own rules → `open`; a ready-made match loop → `duel`/`arena`/`teams`.**
|
|
100
100
|
Presets: **open** (a room of N players — the server owns only seating/capacity/refill, you write
|
|
101
101
|
everything else), **duel** (1v1 winner-stays), **arena** (N-player FFA, join-anytime), **teams**
|
|
102
|
-
(
|
|
102
|
+
(N-v-N with a server-owned match loop + eviction — only when you want that whole loop ready-made;
|
|
103
|
+
for your own team game use `open` + the team section below), **private** (invite-code lobby). Win conditions (batteries-included presets only):
|
|
103
104
|
**lastStanding** (last one alive), **firstToScore** (first to the score target), **highScoreInTime**
|
|
104
105
|
(top score at the time cap), **firstToFinish** (first to finish). A round that hits the time cap
|
|
105
106
|
undecided is a draw. (`open` has no win condition — it never ends a round for you.)
|
|
@@ -120,14 +121,17 @@ build teams/rounds/scoring/win-logic in game code on the primitives you already
|
|
|
120
121
|
```
|
|
121
122
|
|
|
122
123
|
With `open`, `mm.matchmaking.status` only goes `searching`→`waiting`→`playing` (never `countdown`/
|
|
123
|
-
`ended`), `players` is the live roster, and `scores`/`winnerId` stay
|
|
124
|
-
|
|
124
|
+
`ended`), `players` is the live roster, and `teams`/`myTeam`/`scores`/`winnerId` stay EMPTY forever
|
|
125
|
+
— only the server presets fill them. Watch `players` and `status`, not `matchStart`/`matchEnded`
|
|
126
|
+
(they never fire); your teams live in `shared` (mandatory section below), never in
|
|
127
|
+
`mm.matchmaking.teams` — reading that empty map is how every player ends up "on one team".
|
|
128
|
+
Nobody is ever evicted. Build the rest:
|
|
125
129
|
|
|
126
130
|
| Want… | Do it in game code on top of `open` |
|
|
127
131
|
| --- | --- |
|
|
128
132
|
| Room formation & refill | Nothing — the server owns it via `minPlayers`/`maxPlayers`/`fill`. |
|
|
129
133
|
| Duel (1v1) | `maxPlayers: 2, minPlayers: 2`, then start when `mm.matchmaking.players.length === 2`. |
|
|
130
|
-
| Team assignment | The
|
|
134
|
+
| Team assignment | The MANDATORY team section right below this table — the **host** reconciles a balanced `id → team` map into `shared`; every client only reads it. Never computed per-client, never read from `mm.matchmaking.teams` (empty on `open`). |
|
|
131
135
|
| Rounds / countdown | Host writes `{ phase, deadline }` into `shared`; clients render the countdown from the timestamp (no server clock — approximate fairness is fine at this trust tier). |
|
|
132
136
|
| Scores | A host-owned entry in `shared` (or per-player state); you define what a point means. |
|
|
133
137
|
| Win condition | Your code checks its own condition (you already compute the signals) and the host writes the result to `shared`. |
|
|
@@ -135,6 +139,62 @@ With `open`, `mm.matchmaking.status` only goes `searching`→`waiting`→`playin
|
|
|
135
139
|
| Forfeit on disconnect | React to the `players` list shrinking (the SDK surfaces leaves after the reconnect grace). |
|
|
136
140
|
| Spectators | A game-level role: keep a "dead"/observing player seated and just render them as a watcher. There is no server spectator concept — everyone in a room is a player. |
|
|
137
141
|
|
|
142
|
+
#### Team games — the HOST assigns teams into `shared` (MANDATORY, balanced from the first frame)
|
|
143
|
+
|
|
144
|
+
Any team game on `open` (1v1 on opposite sides, 2v2, N-v-N, red-vs-blue) assigns teams in GAME
|
|
145
|
+
CODE — the server never will. Get this wrong and every player lands on one team and the match is
|
|
146
|
+
unplayable. The non-negotiable shape: exactly ONE writer — the current host — keeps an
|
|
147
|
+
`id → teamId` map in `shared`, and every client (host included) READS its own team from that map.
|
|
148
|
+
The assignment is one reconciler that keeps sides balanced at all times: drop leavers, then seat
|
|
149
|
+
every unassigned player on the SMALLEST team, walking the roster in sorted order. From an empty
|
|
150
|
+
map that IS a round-robin split (1v1 → opposite teams, four players → 2+2), and a late joiner
|
|
151
|
+
(`fill: 1`) always lands on the short-handed side. Run it in the host's fixed tick:
|
|
152
|
+
|
|
153
|
+
```ts
|
|
154
|
+
const TEAM_IDS = ["red", "blue"]; // your team set — three or more sides work unchanged
|
|
155
|
+
|
|
156
|
+
function reconcileTeams() { // HOST-only, every tick — cheap; writes only on change
|
|
157
|
+
if (!room.isHost) return;
|
|
158
|
+
const teams = { ...((room.shared.get("teams") ?? {}) as Record<string, string>) };
|
|
159
|
+
const roster = [...room.players.keys()].sort(); // deterministic order
|
|
160
|
+
let changed = false;
|
|
161
|
+
for (const id of Object.keys(teams)) // 1. drop leavers
|
|
162
|
+
if (!roster.includes(id)) { delete teams[id]; changed = true; }
|
|
163
|
+
const size = (t: string) => Object.values(teams).filter((x) => x === t).length;
|
|
164
|
+
for (const id of roster) { // 2. newcomers → the smallest team
|
|
165
|
+
if (teams[id]) continue;
|
|
166
|
+
teams[id] = TEAM_IDS.reduce((a, b) => (size(b) < size(a) ? b : a));
|
|
167
|
+
changed = true;
|
|
168
|
+
}
|
|
169
|
+
if (changed) room.shared.set("teams", teams); // no diff, no write (message budget)
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// EVERY client, in the render loop — read your team, never compute it:
|
|
173
|
+
const myTeam = (room.shared.get("teams") as Record<string, string> | undefined)?.[room.id] ?? null;
|
|
174
|
+
if (myTeam === null) renderNeutral(); // unassigned ≠ team red
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
The failure modes this shape exists to prevent — do NOT do any of these:
|
|
178
|
+
|
|
179
|
+
- **Per-client assignment** (each client picks its own team from `players` order, join time, or
|
|
180
|
+
`Math.random()`): clients see different rosters at different moments, so they all self-assign
|
|
181
|
+
the same side. Team choice has exactly one writer — the host.
|
|
182
|
+
- **Reading `mm.matchmaking.teams` / `myTeam`** — empty/`null` on `open` forever (server presets
|
|
183
|
+
only). A `myTeam ?? "red"` fallback puts EVERYONE on red.
|
|
184
|
+
- **Defaulting the unassigned** (`teams[id] ?? "red"`): between joining and the host's next write
|
|
185
|
+
a player has NO team — render them neutral (or keep the lobby up), never fold them into a side.
|
|
186
|
+
- **Assigning once at match start**: with `fill: 1` the queue keeps seating players mid-game; the
|
|
187
|
+
reconciler runs every tick precisely so joiner #5 lands on the 2-player side, not the 3-player one.
|
|
188
|
+
- **Keeping the map anywhere but `shared`**: host-local state dies with the host's tab. In `shared`
|
|
189
|
+
the map survives host migration — the NEW host's reconciler continues from what's already there
|
|
190
|
+
(nobody's team changes; it only fills gaps).
|
|
191
|
+
|
|
192
|
+
Balance beyond headcount (roles, kits, skill) stays a game decision — headcount balance via this
|
|
193
|
+
reconciler is the floor every team game ships with. **Verify it like the lobby rule:** two browser
|
|
194
|
+
windows on a two-team game MUST land on OPPOSITE teams (different colors, different spawn sides).
|
|
195
|
+
Both on one team = broken — fix it before shipping. Full genre wiring (team spawns, friendly fire,
|
|
196
|
+
team score) is Recipe 4 in [references/genre-recipes.md](references/genre-recipes.md).
|
|
197
|
+
|
|
138
198
|
#### Waiting room / lobby — two patterns
|
|
139
199
|
|
|
140
200
|
`open` seats you into a LIVE shared room the moment you're matched (`session` goes live, players sync)
|
|
@@ -559,6 +619,10 @@ host-driven saving works as long as ANY account is in the room.
|
|
|
559
619
|
- [ ] Waiting room (if any): overlay driven by `mm.matchmaking.status` read every frame, gone the
|
|
560
620
|
moment it flips to `'playing'` — and you WATCHED it close in two browser windows at
|
|
561
621
|
`minPlayers` (never gated on `matchStart` or a host `shared` signal alone).
|
|
622
|
+
- [ ] Team game: the HOST reconciles the balanced `id → team` map into `shared` (leavers dropped,
|
|
623
|
+
newcomers to the smallest team); every client READS its team from `shared` — never computed
|
|
624
|
+
per-client, never `mm.matchmaking.teams`, never a default for the unassigned — and you
|
|
625
|
+
WATCHED two browsers land on OPPOSITE teams.
|
|
562
626
|
- [ ] Picked the matching recipe from [references/genre-recipes.md](references/genre-recipes.md).
|
|
563
627
|
|
|
564
628
|
## Troubleshooting auth
|
|
@@ -132,6 +132,48 @@ sees the correct wave and enemy positions.
|
|
|
132
132
|
|
|
133
133
|
---
|
|
134
134
|
|
|
135
|
+
## Recipe 4 — Team vs team (1v1 sides, 2v2, N-v-N: team deathmatch, CTF, team football)
|
|
136
|
+
|
|
137
|
+
Two (or more) sides; players on a side cooperate. The whole recipe hangs off ONE fact: **teams are
|
|
138
|
+
assigned by the HOST into `shared`, balanced from the first frame** — the mandatory team section in
|
|
139
|
+
SKILL.md is the assignment story, verbatim. Everything else is Recipe 1/2 mechanics filtered by team.
|
|
140
|
+
|
|
141
|
+
| Thing | Channel | Authority |
|
|
142
|
+
| --- | --- | --- |
|
|
143
|
+
| Each player's avatar (+ `uid`, hp, `life`) | `me.set` / `players` | each player |
|
|
144
|
+
| The team map (`id → red/blue`) | `shared` (`"teams"`) | the **host** — the SKILL.md reconciler, every tick |
|
|
145
|
+
| Team scores | `shared` (exactly-once marker — SKILL.md host section) | `room.isHost` |
|
|
146
|
+
| Attacks / defeats | `send` (Recipe 2 rules verbatim) | attacker / victim |
|
|
147
|
+
| A contested ball / flag / payload | `objects` (Recipe 1 rules) | current owner |
|
|
148
|
+
|
|
149
|
+
**Decisions:**
|
|
150
|
+
- **Matchmaking config is only headcount:** `preset: "open"` with `maxPlayers` = team size × team
|
|
151
|
+
count, `minPlayers` = the count the game is playable at (`2` starts a 2v2 short-handed as a 1v1
|
|
152
|
+
and refills to full; `4` waits for a full lobby — pick one deliberately). The server seats
|
|
153
|
+
players; it will NEVER assign teams — that is your host code.
|
|
154
|
+
- **Run the SKILL.md team reconciler in the host tick** — sorted roster + smallest-team =
|
|
155
|
+
round-robin balance, late joiners to the short side, the map in `shared` survives host
|
|
156
|
+
migration. Do not re-derive teams anywhere else, and never read `mm.matchmaking.teams` (empty
|
|
157
|
+
on `open`).
|
|
158
|
+
- **Everything team-flavored READS the map:** tint/skin by `teams[id]`, spawn each player on their
|
|
159
|
+
team's side (respawn via `me.snap`), gate friendly fire in the hit-test — skip targets where
|
|
160
|
+
`teams[target] === teams[me]` before applying damage — and frame the HUD ("your team" vs
|
|
161
|
+
"enemy") from your own entry. A player not yet in the map renders neutral and takes no damage;
|
|
162
|
+
never fold them into a side.
|
|
163
|
+
- **Score per TEAM, not per player:** the host turns Recipe 2's defeat events (or goals/captures)
|
|
164
|
+
into a team point with the exactly-once marker write, keyed by the team id; per-player stats can
|
|
165
|
+
ride alongside keyed by the stable `uid`.
|
|
166
|
+
- **Win + rematch are host-written `shared` facts:** the host checks its own condition (first to
|
|
167
|
+
N, timer end) and writes `shared.set("result", { winner, at })`; everyone renders it. For a
|
|
168
|
+
rematch the host clears `result` and the scores — keep the team map so nobody's side flips.
|
|
169
|
+
|
|
170
|
+
**Acceptance feel:** two browsers land on OPPOSITE teams instantly (the 1v1 case); a third and
|
|
171
|
+
fourth joiner alternate sides (never 3v1); killing the host's tab mid-match changes nobody's team;
|
|
172
|
+
the team score survives the host change; a just-joined player is neutral for a beat, then snaps to
|
|
173
|
+
the short-handed side.
|
|
174
|
+
|
|
175
|
+
---
|
|
176
|
+
|
|
135
177
|
## Not sure which? Start from the table
|
|
136
178
|
|
|
137
179
|
Whatever the genre, ask per thing: *is it one player's own state* (`me.set`) *· a moving thing
|
|
@@ -25,8 +25,10 @@ Three.js release or branch, and do not blindly copy demo architecture.
|
|
|
25
25
|
decided silently. Enqueue the Tier-2 UI generations now with `--no-wait`
|
|
26
26
|
(pick them up later with `npx genex wait <id>`) — they render server-side
|
|
27
27
|
while you build the game. FIRST of all art: the gate's **concept frame** —
|
|
28
|
-
|
|
29
|
-
|
|
28
|
+
a playable-moment shot (verb + threat + objective in frame, per the
|
|
29
|
+
game-ui gate), shown to the user for a keep-or-change answer the moment
|
|
30
|
+
it lands; later `--edit`-able generations anchor to it for STYLE while
|
|
31
|
+
the game contract owns content. Then the `$genex-ai-hud` Stage-1 mockup, enqueued
|
|
30
32
|
here for EVERY game; and the `$genex-ai-menu` video whenever the menu
|
|
31
33
|
decision is yes (the default for every game — "it's only a draft" is not a
|
|
32
34
|
reason to decide no). Skipping this enqueue is
|
|
@@ -50,8 +52,8 @@ Three.js release or branch, and do not blindly copy demo architecture.
|
|
|
50
52
|
- **references**: name 2–3 AAA games whose look this game borrows
|
|
51
53
|
(conventions, lighting mood, palette, post — never trade dress); the
|
|
52
54
|
same 2–3 the UI gate named, extended from the interface to the scene,
|
|
53
|
-
and embodied in the gate's concept frame — the image the scene
|
|
54
|
-
judged against;
|
|
55
|
+
and embodied in the gate's concept frame — the image the scene's STYLE
|
|
56
|
+
is judged against (content always comes from the game contract);
|
|
55
57
|
- **ambient motion**: name ONE subtle environmental motion loop that keeps
|
|
56
58
|
the scene alive at rest — an emissive pulse along edges, heat shimmer,
|
|
57
59
|
drifting dust, a slowly flowing texture. Shader/procedural, zero
|