mirror of
https://github.com/djdevin/recflare.git
synced 2026-09-08 06:31:27 -07:00
[econ] debugging challenges
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
---
|
||||
name: weekly-challenge-config
|
||||
description: Read and author the `Config` rule tree in apps/econ/static/weekly-challenge.json — node types, scene-id predicates, and the shared-scene traps
|
||||
description: Read and author the `Config` rule tree in apps/econ/static/weekly-challenge.json — the full node-type enum, event types, event variables, named scene constants, and the shared-scene traps
|
||||
---
|
||||
|
||||
# The weekly-challenge `Config` rule tree
|
||||
@@ -13,9 +13,21 @@ gameplay, and posts the tree back to `/api/challenge/v2/updateProgress` with its
|
||||
So the tree is a _specification handed to the client_, and a malformed one fails silently —
|
||||
the challenge just never completes. Nothing server-side will tell you.
|
||||
|
||||
Everything here was read off one captured live rotation, not a spec. Meanings marked
|
||||
_(inferred)_ are read from how values line up with the strings the client renders; the rest
|
||||
are pinned by the data.
|
||||
## Provenance
|
||||
|
||||
Two independent sources, and it matters which one a fact came from:
|
||||
|
||||
- **`CoffeeMan240/RecRoom.ChallengeLib`** — https://github.com/CoffeeMan240/RecRoom.ChallengeLib,
|
||||
a .NET builder library reverse-engineered from the 20200306 client with data types from
|
||||
20210813. It names every node type, field and enum below. Facts from it are marked **(lib)**
|
||||
and are *names*, not observations: the library is a reimplementation, so a name can be right
|
||||
about intent and still diverge from what the 20230414 client this server targets actually
|
||||
reads. See "Where the lib and the live data disagree".
|
||||
- **One captured live rotation** — the shipped `ChallengeMapId: 17`. Facts from it are marked
|
||||
**(captured)** and are pinned by data actually served to a real client.
|
||||
|
||||
Where both agree the fact is solid. Where only the lib has it, treat the name as a strong
|
||||
hypothesis and test in-game before shipping a rotation that depends on it.
|
||||
|
||||
## `Config` is an escaped JSON string
|
||||
|
||||
@@ -38,63 +50,173 @@ bun -e 'const c=require("./apps/econ/static/weekly-challenge.json");
|
||||
for (const x of c.Challenges) console.log(x.ChallengeId, x.Description, "\n ", JSON.parse(x.Config))'
|
||||
```
|
||||
|
||||
## Node types
|
||||
## Node types (`ct`) — the full enum
|
||||
|
||||
Each node carries a numeric type in `ct`. Two composite kinds appear:
|
||||
**(lib)** `ChallengeTypes`. Every node carries one. Bold rows are the ones the captured
|
||||
rotation actually uses.
|
||||
|
||||
- **Match** (`ct: 0`) — `wc` is a list of predicates that must _all_ hold for one game
|
||||
result (AND).
|
||||
- **Counter** (`ct: 1`) — `ctc` holds the child node to count, `t` is the target count.
|
||||
| `ct` | Name | Kind | Extra fields |
|
||||
| ---- | ---- | ---- | ------------ |
|
||||
| **`0`** | `Challenge` | Composite — the plain AND node | `wc`, `rc` |
|
||||
| **`1`** | `ChallengeCountChallenge` | Composite — count to a target | `ctc`, `t`, + `wc`/`rc` |
|
||||
| **`2`** | `TimedBufferChallenge` | Composite — count within a rolling window | `ccc`, `t`, `i`, `pm`, `n`, `pb`, `m`, + `wc`/`rc` |
|
||||
| `3` | `DynamicFloatArithmeticChallenge` | Leaf — compare two float resolvers | `op`, `rA`, `rB` |
|
||||
| `4` | `DynamicIntArithmeticChallenge` | Leaf — compare two int resolvers | `op`, `rA`, `rB` |
|
||||
| `5` | `RequiredToolChallenge` | Leaf — **removed** mid-2020; see note | `vs` |
|
||||
| **`6`** | `RequiredEventTypeChallenge` | Leaf — which gameplay event | `vs` (ChallengeEventTypes) |
|
||||
| **`7`** | `RequiredRoomSceneLocationChallenge` | Leaf — scene allow-list | `vs` (`[{"l": guid}]`) |
|
||||
| `8` | `RequiredEnemyTypeChallenge` | Leaf — which enemy | `vs` (EnemyTypes) |
|
||||
| **`9`** | `BoolVarEqualsChallenge` | Leaf — a bool session var equals | `v`, `vs` |
|
||||
| `10` | — | **unnamed in the enum**; do not use | — |
|
||||
| `11` | `DiscGolfFinishUnderParChallenge` | Leaf — no fields at all | — |
|
||||
| `12` | `RequiredGameModeActivityChallenge` | Leaf — legacy game mode | `vs` (LegacyGameModeType) |
|
||||
| `13` | `CompleteGameWithoutChallenge` | Macro over `ct:2` | as `ct:2` |
|
||||
| `14` | `RequiredGestureChallenge` | Leaf — a gesture var (`v: "hg"`) | `v`, `vs` (PlayerGesture) |
|
||||
| `15` | `HitstreakChallenge` | Macro over `ct:1` | as `ct:1` |
|
||||
| `16` | `HitstreakCountChallenge` | Macro over `ct:2` | as `ct:2` |
|
||||
|
||||
Which slot a node uses (`wc` vs `ctc`) tells you what its children are; a node never has
|
||||
both. `ipc` is `false` on every composite node in the reference data — purpose unknown, but
|
||||
the client echoes it back, so keep emitting it.
|
||||
`ct: 5` is obsolete — the lib marks it `LEGACYRequiredToolChallenge`, removed from the game
|
||||
around mid-late 2020. Post-2020 clients express "used tool X" as a `ct:0` with a
|
||||
`PickedUpTool` event predicate plus a `ct:4` comparing the `t_t` var. Its value enum
|
||||
(`SpawnableToolTypes`) **re-rolls every game build**, so any tool-typed challenge is
|
||||
build-specific.
|
||||
|
||||
## Predicate leaves
|
||||
The macros (`13`, `15`, `16`) serialize as their base type plus preset children — they are
|
||||
authoring conveniences, not distinct client behavior. But they emit their own `ct`, so the
|
||||
client must know the id: don't invent macro ids.
|
||||
|
||||
Leaves carry `vs`, a list of accepted values matched as OR.
|
||||
## Fields
|
||||
|
||||
| `ct` | Shape | Meaning |
|
||||
| ---- | -------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `6` | `{"ct":6,"vs":[2]}` | _(inferred)_ The kind of event being matched — a finished game/session. Present in **every** leaf group and always `[2]`; nothing observed varying it, so treat it as required boilerplate. |
|
||||
| `7` | `{"ct":7,"vs":[{"l":"<guid>"}]}` | Scene allow-list: each `l` is a subroom's `UnitySceneId` (see `apps/rooms`). Matches if the game happened in any of them. |
|
||||
| `9` | `{"ct":9,"vs":[true],"v":"won"}` | A named session variable (`v`) equals one of `vs` — here, the player won. |
|
||||
| Field | Meaning |
|
||||
| ----- | ------- |
|
||||
| `ct` | Node type, above. **(lib + captured)** |
|
||||
| `ipc` | `IgnorePreviousCompletions` **(lib)** — the captured data sets it `false` on every composite. Keep emitting it. |
|
||||
| `wc` | `WithConditions` — predicates that must **all** hold (AND). **(lib + captured)** |
|
||||
| `rc` | `ResetConditions` — matching any of these **resets progress to zero**. This is what makes streaks. **(lib)** |
|
||||
| `ctc` | `ChallengesToCount` — `ct:1`'s children; each match increments toward `t`. **(lib + captured)** |
|
||||
| `ccc` | `ChallengesToCount` for `ct:2` — same idea, different slot name. **(lib)** |
|
||||
| `t` | Target count. **(lib + captured)** |
|
||||
| `vs` | Accepted values, matched as OR. **(lib + captured)** |
|
||||
| `v` | Var key for `ct:9`/`ct:14`. **(lib + captured)** |
|
||||
| `in` | `Inclusive` — omitted when false. **(lib)**; the captured `won` predicate omits it. |
|
||||
| `ex` | `ExcludesIncludesNull` — omitted when false. **(lib)** |
|
||||
| `i` | `ct:2` window length in seconds, as a **2-dp string** (`"-1.00"`). `-1` = no window. **(lib)** |
|
||||
| `pm` | `ct:2` progress mode: `0` Complete, `1` Count. **(lib)** |
|
||||
| `n` | `ct:2` notification counts — milestones the client announces en route to `t`. **(lib)** |
|
||||
| `pb` | `ct:2` `PersistBuffer` — carry the buffer across games. **(lib)** |
|
||||
| `m` | `ct:2` count method, omitted when `0`. **(lib)** |
|
||||
| `op` | `ct:3`/`ct:4` comparison: `0` GT, `1` LT, `2` EQ, `3` GTE, `4` LTE. **(lib)** |
|
||||
| `rA`, `rB` | `ct:3`/`ct:4` operands, each a num resolver. **(lib)** |
|
||||
| `cc`, `c` | **Client-side progress — never author these.** See below. **(captured)** |
|
||||
|
||||
## The two idioms
|
||||
A num resolver (`rA`/`rB`) is `{"t":0,"c":<const>}` for a constant or `{"t":1,"vk":"<var>"}`
|
||||
for a session variable. Note `t` means *resolver type* here, not target.
|
||||
|
||||
Every challenge in the captured rotation is one of these.
|
||||
`m` values **(lib)**: `0` Count, `1` UniqueToolCount, `2` UniqueAttackerCount,
|
||||
`3` UniqueDefenderCount, `4` GroupedToolMaxCount, `5` UniqueGameCount.
|
||||
|
||||
```jsonc
|
||||
// "Complete ^TheRiseOfJumbotron quest" — one winning session in one scene
|
||||
{ "ct": 0, "ipc": false, "wc": [
|
||||
{ "ct": 6, "vs": [2] },
|
||||
{ "ct": 9, "vs": [true], "v": "won" },
|
||||
{ "ct": 7, "vs": [{ "l": "acc06e66-…" }] } // TheRiseofJumbotron / Home
|
||||
]}
|
||||
## Event types — `ct: 6` `vs` values
|
||||
|
||||
// "Complete 5 Charades games" — count matching sessions to a target
|
||||
{ "ct": 1, "ipc": false, "t": 5, "ctc": [
|
||||
{ "ct": 0, "ipc": false, "wc": [
|
||||
{ "ct": 6, "vs": [2] },
|
||||
{ "ct": 7, "vs": [{ "l": "a673712c-…" }, { "l": "4078dfed-…" }] } // 3DCharades + Legacy3DCharades
|
||||
]}
|
||||
]}
|
||||
```
|
||||
**(lib)** `ChallengeEventTypes`. The captured rotation only ever used `2`, which the old
|
||||
version of this doc guessed was opaque boilerplate. It is not — it is `GameEnd`:
|
||||
|
||||
The quest challenges have **no `t`** (one qualifying session is the whole goal) and the
|
||||
counted ones have **no `won` predicate** (finishing counts, winning is irrelevant). A "one
|
||||
map only" challenge is the counted shape with a single-entry scene list.
|
||||
| id | Name | | id | Name |
|
||||
| -- | ---- | - | -- | ---- |
|
||||
| `0` | `None` | | `7` | `Score` |
|
||||
| `1` | `GameStart` | | `8` | `ShieldBlock` |
|
||||
| **`2`** | **`GameEnd`** | | `9` | `ActivityLoad` |
|
||||
| `3` | `LocalPlayerEliminated` | | `10` | `FlagCaptured` |
|
||||
| `4` | `ElminatedOtherPlayer` _(sic)_ | | `11` | `FlagReturned` |
|
||||
| `5` | `EliminatedAI` | | `12` | `Gesture` |
|
||||
| `6` | `PickedUpTool` | | `13` | `PlayerJoined` |
|
||||
|
||||
## Scene ids, not room ids
|
||||
`{"ct":6,"vs":[2]}` appears in every captured leaf group because every captured challenge
|
||||
counts *finished games*, not because the field is fixed. Counting anything else — hits,
|
||||
scores, revives — means changing this value.
|
||||
|
||||
`ct: 7` matches `UnitySceneId`, so one guid can name several rooms — a screens room, its VR
|
||||
twin, and the standalone base room all share a scene. The captured "Complete 10 games in
|
||||
^Paintball" lists six guids, which are the subrooms of _both_ `Paintball` and `PaintballVR`,
|
||||
each of which is also a standalone base room (`River`, `Clearcut`, …). One list covers every
|
||||
way in.
|
||||
## Session variables (`v`, `vk`)
|
||||
|
||||
Resolve a guid against `SubRooms[].UnitySceneId` in `apps/rooms/static/ImportRooms.json`
|
||||
(same data as `apps/rooms/migrations/0002_import_rooms.sql`). Run from the repo root:
|
||||
**(lib)** The vars an event publishes, keyed by the event that carries them.
|
||||
|
||||
| Key | Type | On | Meaning |
|
||||
| --- | ---- | -- | ------- |
|
||||
| `gid` | string | any game event | Game id |
|
||||
| `gameMode` | int | any game event | `LegacyGameModeType` |
|
||||
| `numTeammates` | int | any game event | Size of your team |
|
||||
| `t_score` | float | any game event | Your team's score |
|
||||
| `jip` | bool | GameStart | Joined in progress |
|
||||
| `isSpectator` | bool | GameStart | Spectating |
|
||||
| `te` | float | GameEnd | Time elapsed |
|
||||
| **`won`** | bool | GameEnd | Did the player win (used in quests) |
|
||||
| `ev_score` | float | Score | Current score |
|
||||
| `e_vid` | int | enemy events | Enemy photon view id |
|
||||
| `e_t` | int | enemy events | `EnemyTypes` |
|
||||
| `dp_vid` / `dp_pid` | int | player events | Defender photon view id / RecNet id |
|
||||
| `ap_vid` / `ap_pid` | int | player events | Attacker photon view id / RecNet id |
|
||||
| `bodyPart` | int | ElminatedOtherPlayer | `BodyPart`: `-1` None, `0` Head, `1` Torso, `2` LeftHand, `3` RightHand, `4` Mouth |
|
||||
| `t_vid` / `t_t` | int | tool events | Tool photon view id / `SpawnableToolTypes` |
|
||||
| `strokeCount` / `par` | int | DiscGolf Score | Strokes taken / hole par |
|
||||
|
||||
So the captured `{"ct":9,"vs":[true],"v":"won"}` is `GameEnded.Won` — a quest win.
|
||||
|
||||
## Named scene constants — `ct: 7` `vs` values
|
||||
|
||||
**(lib)** `RoomSceneLocations`, cross-checked against `apps/rooms/static/ImportRooms.json`.
|
||||
`ct:7` matches `UnitySceneId`, so **one guid can name several rooms**. Bolded rooms share
|
||||
their scene — a challenge naming that guid completes in every room listed.
|
||||
|
||||
The lib names every scene present in our room data (36 of its 40 resolve; the other four are
|
||||
marked below), so this table is a complete index in both directions.
|
||||
|
||||
| Constant | Scene id | Rooms in `ImportRooms.json` |
|
||||
| -------- | -------- | --------------------------- |
|
||||
| `DORM_ROOM` | `76d98498-60a1-430c-ab76-b54a29b7a163` | DormRoom/Home |
|
||||
| `REC_CENTER` | `cbad71af-0831-44d8-b8ef-69edafa841f6` | RecCenter/Home |
|
||||
| `LEGACY_CHARADES` | `4078dfed-24bb-4db7-863f-578ba48d726b` | Legacy3DCharades/Home |
|
||||
| `LAKE` | `f6f7256c-e438-4299-b99e-d20bef8cf7e0` | **DiscGolfLake/Home**, **Lake/Home** |
|
||||
| `PROPULSION` | `d9378c9f-80bc-46fb-ad1e-1bed8a674f55` | **DiscGolfPropulsion/Home**, **PropulsionTestRange/Home** |
|
||||
| `DODGEBALL` | `3d474b26-26f7-45e9-9a36-9b02847d5e6f` | **Dodgeball/Home**, **Gym/Home**, **DodgeballVR/Home** |
|
||||
| `THE_LOUNGE` | `a067557f-ca32-43e6-b6e5-daaec60b4f5a` | Lounge/Home |
|
||||
| `PADDLEBALL` | `d89f74fa-d51e-477a-a425-025a891dd499` | Paddleball/Home |
|
||||
| `RIVER` | `e122fe98-e7db-49e8-a1b1-105424b6e1f0` | **Paintball/River**, **PaintballVR/River**, **River/Home** |
|
||||
| `HOMESTEAD` | `a785267d-c579-42ea-be43-fec1992d1ca7` | **Paintball/Homestead**, **PaintballVR/Homestead**, **Homestead/Home** |
|
||||
| `QUARRY` | `ff4c6427-7079-4f59-b22a-69b089420827` | **Paintball/Quarry**, **PaintballVR/Quarry**, **Quarry/Home** |
|
||||
| `CLEAR_CUT` | `380d18b5-de9c-49f3-80f7-f4a95c1de161` | **Paintball/Clearcut**, **PaintballVR/Clearcut**, **Clearcut/Home** |
|
||||
| `SPILLWAY` | `58763055-2dfb-4814-80b8-16fac5c85709` | **Paintball/Spillway**, **PaintballVR/Spillway**, **Spillway/Home** |
|
||||
| `QUEST_FOR_THE_GOLDEN_TROPHY` | `91e16e35-f48f-4700-ab8a-a1b79e50e51b` | GoldenTrophy/Home |
|
||||
| `ORIENTATION` | `c79709d8-a31b-48aa-9eb8-cc31ba9505e8` | Orientation/Home |
|
||||
| `THE_RISE_OF_JUMBOTRON` | `acc06e66-c2d0-4361-b0cd-46246a4c455c` | TheRiseofJumbotron/Home |
|
||||
| `CURSE_OF_THE_CRIMSON_CAULDRON` | `949fa41f-4347-45c0-b7ac-489129174045` | CrimsonCauldron/Home |
|
||||
| `THE_ISLE_OF_LOST_SKULLS` | `7e01cfe0-820a-406f-b1b3-0a5bf575235c` | IsleOfLostSkulls/Home |
|
||||
| `SOCCER` | `6d5eea4b-f069-4ed0-9916-0e2f07df0d03` | **Soccer/Home**, **Stadium/Home** |
|
||||
| `PERFORMANCE_HALL` | `9932f88f-3929-43a0-a012-a40b5128e346` | PerformanceHall/Home |
|
||||
| `PSVR_ROOM_CALIBRATION` | `f5fbd9c9-e853-4036-9d48-5f68e861af04` | _not in ImportRooms.json_ |
|
||||
| `PARK` | `0a864c86-5a71-4e18-8041-8124e4dc9d98` | Park/Home |
|
||||
| `WAREHOUSE` | `239e676c-f12f-489f-bf3a-d4c383d692c3` | **LaserTag/Hangar**, **Hangar/Home** |
|
||||
| `CYBERJUNK_CITY` | `9d6456ce-6264-48b4-808d-2d96b3d91038` | **LaserTag/CyberJunkCity**, **LaserTagCyberJunk/Home**, **CyberJunkCity/Home** |
|
||||
| `MAKER_ROOM` | `a75f7547-79eb-47c6-8986-6767abcb4f92` | MakerRoom/Home |
|
||||
| `FRONTIER_SOLOS` | `b010171f-4875-4e89-baba-61e878cd41e1` | RecRoyaleSolos/Home |
|
||||
| `FRONTIER_SQUADS` | `253fa009-6e65-4c90-91a1-7137a56a267f` | RecRoyaleSquads/Home |
|
||||
| `CRESCENDO_OF_THE_BLOOD_MOON` | `49cb8993-a956-43e2-86f4-1318f279b22a` | Crescendo/Home |
|
||||
| `BOWLING_ALLEY` | `ae929543-9a07-41d5-8ee9-dbbee8c36800` | **Bowling/Home**, **BowlingAlley/Home** |
|
||||
| `ANIMATION_RECORDING_STUDIO` | `a95c349c-0f96-4c2d-a4c8-4969ffa8ea44` | _not in ImportRooms.json_ |
|
||||
| `STUNTRUNNER` | `b7281665-a715-4051-826b-8e08e69c6172` | StuntRunner/StuntRunner |
|
||||
| `STUNTRUNNER_THE_MAIN_EVENT` | `3a636bd2-f896-424c-9225-c184522c0d87` | StuntRunner/TheMainEvent |
|
||||
| `STUNTRUNNER_BASE_ROOM` | `882e9b96-7115-4b03-86f6-c0c9d8e22e00` | StuntRunnerBaseRoom/Home |
|
||||
| `REGISTRATION` | `cf61556d-68fd-4288-9ae5-7a512621e569` | Registration/Home |
|
||||
| `AR_ROOM` | `bf268f5f-b55b-41af-8628-32fa4b5d70b6` | ARRoom/Home |
|
||||
| `DRIVEIN` | `65ddbb48-5a01-4e3e-972d-e5c7419e2bc3` | **Paintball/Drive-in**, **PaintballVR/Drive-in**, **DriveIn/Home** |
|
||||
| `CHARADES_THE_INK_SPACE` | `a673712c-877f-4749-b69a-4a4c6310d545` | 3DCharades/InkSpaceHome |
|
||||
| `THE_INK_SPACE_BASE_ROOM` | `1fa06e3c-c307-4c11-a91b-1fabcddb8a96` | TheInkSpace/Home |
|
||||
| `FRONTIER_UGC` | `a16bfd31-ffb9-46ac-a199-362c163130c0` | _not in ImportRooms.json_ |
|
||||
|
||||
The lib also defines `INVALID`, which serializes as `Guid.Empty` and is not a real scene.
|
||||
|
||||
Two shared scenes are genuine surprises rather than a deliberate screens/VR/base-room trio:
|
||||
**`Soccer/Home` and `Stadium/Home` are the same scene**, and **`Dodgeball` shares its scene
|
||||
with the plain `Gym`**. Decide whether the extra rooms are acceptable before shipping.
|
||||
|
||||
Regenerate the room column from the repo root:
|
||||
|
||||
```sh
|
||||
cat > /tmp/scene.ts <<'EOF'
|
||||
@@ -111,61 +233,114 @@ bun run /tmp/scene.ts 380d18b5-de9c-49f3-80f7-f4a95c1de161
|
||||
# → 380d18b5-… Paintball/Clearcut, PaintballVR/Clearcut, Clearcut/Home
|
||||
```
|
||||
|
||||
With no arguments it dumps every scene, which is how you go the other way — from a room name
|
||||
to the guid to put in `vs`.
|
||||
With no arguments it dumps every scene, which is how you go the other way.
|
||||
|
||||
### Shared scenes to watch for
|
||||
## The idioms
|
||||
|
||||
These guids resolve to more than one room, so a challenge naming one also completes in the
|
||||
others. Most are a deliberate screens/VR/base-room trio, but two are genuine surprises:
|
||||
**`Soccer/Home` and `Stadium/Home` are the same scene**, so a soccer challenge also completes
|
||||
in the Stadium, and `Dodgeball` shares its scene with the plain `Gym`.
|
||||
### One-shot quest **(captured)**
|
||||
|
||||
| Scene id | Rooms |
|
||||
| ----------- | ------------------------------------------------------------------ |
|
||||
| `6d5eea4b…` | Soccer/Home, **Stadium/Home** |
|
||||
| `3d474b26…` | Dodgeball/Home, **Gym/Home**, DodgeballVR/Home |
|
||||
| `ae929543…` | Bowling/Home, BowlingAlley/Home |
|
||||
| `f6f7256c…` | DiscGolfLake/Home, Lake/Home |
|
||||
| `d9378c9f…` | DiscGolfPropulsion/Home, PropulsionTestRange/Home |
|
||||
| `239e676c…` | LaserTag/Hangar, Hangar/Home |
|
||||
| `9d6456ce…` | LaserTag/CyberJunkCity, LaserTagCyberJunk/Home, CyberJunkCity/Home |
|
||||
| `e122fe98…` | Paintball/River, PaintballVR/River, River/Home |
|
||||
| `a785267d…` | Paintball/Homestead, PaintballVR/Homestead, Homestead/Home |
|
||||
| `ff4c6427…` | Paintball/Quarry, PaintballVR/Quarry, Quarry/Home |
|
||||
| `380d18b5…` | Paintball/Clearcut, PaintballVR/Clearcut, Clearcut/Home |
|
||||
| `58763055…` | Paintball/Spillway, PaintballVR/Spillway, Spillway/Home |
|
||||
| `65ddbb48…` | Paintball/Drive-in, PaintballVR/Drive-in, DriveIn/Home |
|
||||
No `t` — one qualifying session is the whole goal.
|
||||
|
||||
Regenerate this list with the script above and no arguments.
|
||||
```jsonc
|
||||
// "Complete ^TheRiseOfJumbotron quest"
|
||||
{ "ct": 0, "ipc": false, "wc": [
|
||||
{ "ct": 6, "vs": [2] }, // GameEnd
|
||||
{ "ct": 9, "vs": [true], "v": "won" }, // …and won
|
||||
{ "ct": 7, "vs": [{ "l": "acc06e66-…" }] } // THE_RISE_OF_JUMBOTRON
|
||||
]}
|
||||
```
|
||||
|
||||
### Counted sessions **(captured)**
|
||||
|
||||
No `won` predicate — finishing counts, winning is irrelevant. A "one map only" challenge is
|
||||
this with a single-entry scene list.
|
||||
|
||||
```jsonc
|
||||
// "Complete 5 Charades games"
|
||||
{ "ct": 1, "ipc": false, "ctc": [
|
||||
{ "ct": 0, "ipc": false, "wc": [
|
||||
{ "ct": 6, "vs": [2] },
|
||||
{ "ct": 7, "vs": [{ "l": "a673712c-…" }, { "l": "4078dfed-…" }] } // both Charades scenes
|
||||
]}
|
||||
], "t": 5 }
|
||||
```
|
||||
|
||||
### Streaks and buffers **(lib)**
|
||||
|
||||
`rc` resets the count, which is how "N in a row without dying" is expressed. Wrapping that
|
||||
in a `ct:2` counts how many streaks you land:
|
||||
|
||||
```jsonc
|
||||
// "Get 20 three-kill streaks in the Golden Trophy quest", announced at 5/10/15
|
||||
{ "ct": 2, "ipc": false,
|
||||
"wc": [{ "ct": 7, "ipc": false, "vs": [{ "l": "91e16e35-…" }] }], // GOLDEN_TROPHY
|
||||
"ccc": [{ "ct": 1, "ipc": false, "t": 3,
|
||||
"ctc": [{ "ct": 6, "ipc": false, "vs": [5] }], // EliminatedAI
|
||||
"rc": [{ "ct": 6, "ipc": false, "vs": [3] }] }],// …reset on your own death
|
||||
"i": "-1.00", // no time window
|
||||
"t": 20, "pm": 1, // count mode, target 20
|
||||
"n": [5, 10, 15], // milestone notifications
|
||||
"pb": true } // buffer survives across games
|
||||
```
|
||||
|
||||
`pm: 1` (Count) is what makes `t` a tally; `pm: 0` (Complete) treats the buffer as a
|
||||
one-shot. `i: "-1.00"` disables the time window — a positive value makes it "N within
|
||||
X seconds".
|
||||
|
||||
## Progress fields (`cc`, `c`) — client-side only
|
||||
|
||||
On `updateProgress` the client posts the same tree back with its own progress written into
|
||||
it: **`cc`** on the counter node is the current count (`…,"t":5,"cc":1`), and **`c`**
|
||||
(`"c":true`) marks a node it now considers satisfied.
|
||||
it: **`cc`** on a counter is the current count (`…,"t":5,"cc":1`), and **`c`** (`"c":true`)
|
||||
marks a node it now considers satisfied.
|
||||
|
||||
Neither belongs in `weekly-challenge.json` — they are progress, not definition. The server
|
||||
echoes the posted `Config` back untouched and never persists it (`challenge_status` stores
|
||||
only the completion flag; see `apps/econ/src/challenge-db.ts`), so the running count lives
|
||||
only in the client. Don't add `cc`/`c` to an authored tree, and don't try to read progress
|
||||
out of one.
|
||||
only in the client. Don't author `cc`/`c`, and don't try to read progress out of one.
|
||||
|
||||
This is also the cheapest way to decode an unfamiliar tree: serve it, play the activity, and
|
||||
watch which node grows a `cc`.
|
||||
|
||||
## Where the lib and the live data disagree
|
||||
|
||||
The library is a 2020/2021 reimplementation, not the 20230414 client. Known divergences,
|
||||
all worth checking before trusting a lib-only field:
|
||||
|
||||
- **`BoolVarEqualsChallenge` and `RequiredGestureChallenge` serialize `ct: 0` in the lib.**
|
||||
`RequiredObjectChallenge` declares `ChallengeType` as a getter-only auto-property with no
|
||||
initializer and the `VarEquals` subclasses never override it, so it defaults to `0`. The
|
||||
captured rotation proves the real value is `9` for the `won` predicate. Don't take a
|
||||
lib-generated `ct` for those two at face value.
|
||||
- **`in` is emitted where the captured data omits it.** `BoolVarEqualsChallenge`'s
|
||||
constructor forces `Inclusive = true`, so the lib would write
|
||||
`{"ct":9,"in":true,"vs":[true],"v":"won"}` where the live rotation sent no `in`.
|
||||
- **`ipc` is written twice when true** — `ChallengeBase.Serialize` adds it, then
|
||||
`Challenge.Serialize` adds it again, which throws on the duplicate dictionary key. The lib
|
||||
only works with `IgnorePreviousCompletions = false`, which is all the captured data uses.
|
||||
- **`ChallengeCountChallengeBuilder.ResetCondition` adds the node to itself** — its parameter
|
||||
shadows the `challenge` field. Set `ResetConditions` directly instead.
|
||||
- **`SpawnableToolTypes` re-rolls per build**, so `ct:5` and any `t_t` comparison is pinned
|
||||
to one client version.
|
||||
|
||||
## Authoring a new challenge
|
||||
|
||||
1. Pick the idiom: one-shot (`ct: 0` root, add the `won` predicate if winning is required)
|
||||
or counted (`ct: 1` root with `t`).
|
||||
2. Resolve the scenes with the script above, and check the shared-scene table — decide
|
||||
1. Pick the idiom: one-shot (`ct:0` root, add the `won` predicate if winning is required),
|
||||
counted (`ct:1` root with `t`), or buffered/streak (`ct:2` root, `rc` on the child).
|
||||
2. Resolve the scenes from the table above, and check the bolded shared rooms — decide
|
||||
whether the extra rooms it lets in are acceptable.
|
||||
3. Build the tree as an object, stringify it twice into `Config`.
|
||||
4. Give the entry a `ChallengeId` unique **within the rotation** (they aren't sequential),
|
||||
3. Pick the right event type for `ct:6` — `2` (GameEnd) only if you really are counting
|
||||
finished games.
|
||||
4. Build the tree as an object, stringify it twice into `Config`.
|
||||
5. Give the entry a `ChallengeId` unique **within the rotation** (they aren't sequential),
|
||||
and write the real goal in `Description` — `Name` is an internal slug that is not
|
||||
authoritative (captured id `63` is named `Complete3SpillwayGames` but its `Config` and
|
||||
description are Clearcut).
|
||||
5. Leave `Complete: false`; `getCurrent` stamps it per caller.
|
||||
6. Bump `ChallengeMapId` if this is a new rotation — ids only need to be unique within one,
|
||||
description are Clearcut). **Keep `Description`/`Tooltip` in step with `Config`:** the
|
||||
client renders the strings and evaluates the tree independently, so a mismatch ships a
|
||||
challenge that advances somewhere the text never mentions.
|
||||
6. Leave `Complete: false`; `getCurrent` stamps it per caller.
|
||||
7. Bump `ChallengeMapId` if this is a new rotation — ids only need to be unique within one,
|
||||
and a new map id is what resets stored completions.
|
||||
7. Keep `ServerTime` inside `StartAt`…`EndAt`, or the client renders the rotation as expired.
|
||||
8. Keep `ServerTime` inside `StartAt`…`EndAt`, or the client renders the rotation as expired.
|
||||
|
||||
Sanity check the file parses and every tree parses:
|
||||
|
||||
@@ -174,10 +349,21 @@ bun -e 'const c=require("./apps/econ/static/weekly-challenge.json");
|
||||
c.Challenges.forEach(x => JSON.parse(x.Config)); console.log("ok", c.Challenges.length)'
|
||||
```
|
||||
|
||||
Then `bun turbo -F econ test` — `src/test/integration/api.test.ts` imports the file and
|
||||
asserts `getCurrent` against it.
|
||||
Then `bun vitest run apps/econ` — `src/test/integration/api.test.ts` imports the file and
|
||||
asserts `getCurrent` against it. Note the gift threshold follows the rotation size
|
||||
(`CHALLENGES_REQUIRED_FOR_GIFT` clamps to what you publish), so a rotation of three or fewer
|
||||
asks for all of them.
|
||||
|
||||
## Credits
|
||||
|
||||
The node-type, event-type, field, variable and scene-constant tables above are derived from
|
||||
**[CoffeeMan240/RecRoom.ChallengeLib](https://github.com/CoffeeMan240/RecRoom.ChallengeLib)**,
|
||||
a .NET challenge-builder library that reverse-engineered this format from the 20200306 client
|
||||
(data types from 20210813). Without it the `ct` values were opaque integers. Thanks to
|
||||
CoffeeMan240 for publishing it.
|
||||
|
||||
## Related
|
||||
|
||||
- `apps/econ/README.md` — the rest of the weekly-challenge file (top level, `Gift`, progress)
|
||||
- `.agents/daily-objectives/SKILL.md` — the other objective system, on `GET api/config/v2`
|
||||
- `.agents/skills/daily-objectives-config/SKILL.md` — the other objective system, on
|
||||
`GET api/config/v2`. Different grammar entirely: a flat `{type, score}` enum, not a tree.
|
||||
|
||||
+9
-8
@@ -248,10 +248,11 @@ rotation to differ.
|
||||
| `FallbackGiftName` | `"4-Star Box"` | Shown when the client can't resolve `Gift` into a name. |
|
||||
| `ChallengeThemeString` | a designer quote | Free text carried through from the captured rotation; a theme note, not a rendered UI string as far as we can tell. |
|
||||
|
||||
**The frozen clock:** `ServerTime` (Mar 31) sits _inside_ `StartAt`…`EndAt` (Mar 25 → Apr 1),
|
||||
about a day before the end, and the file is static — so the client always sees an active
|
||||
rotation with a ~1-day countdown rather than an expired one. If you edit the window, move
|
||||
`ServerTime` inside the new one too, or the challenges may render as already over.
|
||||
**The frozen clock:** `ServerTime` sits _inside_ `StartAt`…`EndAt`, about a day before the
|
||||
end, and the file is static — so the client always sees an active rotation with a ~1-day
|
||||
countdown rather than an expired one (the captured week: Mar 31 inside Mar 25 → Apr 1). If
|
||||
you edit the window, move `ServerTime` inside the new one too, or the challenges may render
|
||||
as already over.
|
||||
|
||||
### A challenge entry
|
||||
|
||||
@@ -279,10 +280,10 @@ scene allow-list (`ct: 7`, subroom `UnitySceneId`s) or a session variable (`ct:
|
||||
`won`). The server never evaluates any of it — the client does, and posts the tree back with
|
||||
its own count written in.
|
||||
|
||||
**Reading or writing one? See `.agents/weekly-challenge-config/SKILL.md`** — the full
|
||||
grammar, the two idioms the file uses, how to resolve a scene guid to a room, the shared
|
||||
scenes that make a challenge complete in more rooms than you meant (`Soccer` and `Stadium`
|
||||
are one scene), and an authoring checklist.
|
||||
**Reading or writing one? See `.agents/skills/weekly-challenge-config/SKILL.md`** — the full
|
||||
node-type enum, the event types and session variables a predicate can match, the named scene
|
||||
constants (including the shared scenes that make a challenge complete in more rooms than you
|
||||
meant — `Soccer` and `Stadium` are one scene), the three idioms, and an authoring checklist.
|
||||
|
||||
### The `Gift` block
|
||||
|
||||
|
||||
@@ -1985,9 +1985,9 @@ describe('econ endpoints', () => {
|
||||
test('completing enough of the rotation grants its gift, once', async () => {
|
||||
// The live rotation, so this follows whatever static/weekly-challenge.json holds.
|
||||
const { ids, report } = await finishTheRotation('74')
|
||||
// The whole point of the threshold: the gift lands before the set is finished (the
|
||||
// published week is five challenges for three).
|
||||
expect(REQUIRED_FOR_GIFT).toBeLessThan(ids.length)
|
||||
// The threshold can't ask for more than the week publishes: a five-challenge week asks
|
||||
// for three, and a rotation of three or fewer asks for all of them.
|
||||
expect(REQUIRED_FOR_GIFT).toBeLessThanOrEqual(ids.length)
|
||||
for (const id of ids.slice(0, REQUIRED_FOR_GIFT - 1)) {
|
||||
expect((await report(id)).status).toBe(200)
|
||||
}
|
||||
|
||||
@@ -1,48 +1,24 @@
|
||||
{
|
||||
"ChallengeMapId": 17,
|
||||
"ChallengeMapId": 19,
|
||||
"CompletedRequired": false,
|
||||
"StartAt": "2026-03-25T21:00:00",
|
||||
"EndAt": "2026-04-01T21:00:00",
|
||||
"ServerTime": "2026-03-31T14:42:54.2754728Z",
|
||||
"StartAt": "2026-08-19T21:00:00",
|
||||
"EndAt": "2026-09-26T21:00:00",
|
||||
"ServerTime": "2026-08-25T14:42:54.2754728Z",
|
||||
"Challenges": [
|
||||
{
|
||||
"ChallengeId": 37,
|
||||
"Name": "CompleteJT",
|
||||
"Config": "{\"ct\":1,\"c\":true,\"ipc\":false,\"ctc\":[{\"ct\":0,\"ipc\":false,\"wc\":[{\"ct\":6,\"vs\":[2]},{\"ct\":7,\"vs\":[{\"l\":\"6d5eea4b-f069-4ed0-9916-0e2f07df0d03\"},{\"l\":\"4078dfed-24bb-4db7-863f-578ba48d726b\"}]}]}],\"t\":1,\"cc\":1}",
|
||||
"Description": "Complete ^TheRiseOfJumbotron quest",
|
||||
"Tooltip": "Vanquish Jumbotron in the Rise of Jumbotron quest!",
|
||||
"ChallengeId": 1,
|
||||
"Name": "Debug2AIGoldenTrophy",
|
||||
"Config": "{\"ct\":1,\"ipc\":false,\"ctc\":[{\"ct\":0,\"ipc\":false,\"wc\":[{\"ct\":6,\"vs\":[5]},{\"ct\":7,\"vs\":[{\"l\":\"91e16e35-f48f-4700-ab8a-a1b79e50e51b\"}]}]}],\"t\":2}",
|
||||
"Description": "Defeat 2 enemies in ^GoldenTrophy",
|
||||
"Tooltip": "Go to ^GoldenTrophy and take out 2 goblins.",
|
||||
"Complete": false
|
||||
},
|
||||
{
|
||||
"ChallengeId": 38,
|
||||
"Name": "CompleteBloodMoon",
|
||||
"Config": "{\"ct\":0,\"ipc\":false,\"wc\":[{\"ct\":6,\"vs\":[2]},{\"ct\":9,\"vs\":[true],\"v\":\"won\"},{\"ct\":7,\"vs\":[{\"l\":\"49cb8993-a956-43e2-86f4-1318f279b22a\"}]}]}",
|
||||
"Description": "Complete the ^Crescendo quest",
|
||||
"Tooltip": "Vanquish Dracula in the Crescendo of the Blood Moon quest!",
|
||||
"Complete": false
|
||||
},
|
||||
{
|
||||
"ChallengeId": 44,
|
||||
"Name": "Complete10PaintballGames",
|
||||
"Config": "{\"ct\":1,\"ipc\":false,\"ctc\":[{\"ct\":0,\"ipc\":false,\"wc\":[{\"ct\":6,\"vs\":[2]},{\"ct\":7,\"vs\":[{\"l\":\"e122fe98-e7db-49e8-a1b1-105424b6e1f0\"},{\"l\":\"a785267d-c579-42ea-be43-fec1992d1ca7\"},{\"l\":\"ff4c6427-7079-4f59-b22a-69b089420827\"},{\"l\":\"380d18b5-de9c-49f3-80f7-f4a95c1de161\"},{\"l\":\"58763055-2dfb-4814-80b8-16fac5c85709\"},{\"l\":\"65ddbb48-5a01-4e3e-972d-e5c7419e2bc3\"}]}]}],\"t\":10}",
|
||||
"Description": "Complete 10 games in ^Paintball",
|
||||
"Tooltip": "Go to ^Paintball and finish 10 games across any maps.",
|
||||
"Complete": false
|
||||
},
|
||||
{
|
||||
"ChallengeId": 49,
|
||||
"Name": "Complete5Charades",
|
||||
"Config": "{\"ct\":1,\"ipc\":false,\"ctc\":[{\"ct\":0,\"ipc\":false,\"wc\":[{\"ct\":6,\"vs\":[2]},{\"ct\":7,\"vs\":[{\"l\":\"a673712c-877f-4749-b69a-4a4c6310d545\"},{\"l\":\"4078dfed-24bb-4db7-863f-578ba48d726b\"}]}]}],\"t\":5}",
|
||||
"Description": "Complete 5 Charades games",
|
||||
"Tooltip": "Finish 5 games in ^3DCharades.",
|
||||
"Complete": false
|
||||
},
|
||||
{
|
||||
"ChallengeId": 63,
|
||||
"Name": "Complete3SpillwayGames",
|
||||
"Config": "{\"ct\":1,\"ipc\":false,\"ctc\":[{\"ct\":0,\"ipc\":false,\"wc\":[{\"ct\":6,\"vs\":[2]},{\"ct\":7,\"vs\":[{\"l\":\"380d18b5-de9c-49f3-80f7-f4a95c1de161\"}]}]}],\"t\":3}",
|
||||
"Description": "Complete 3 games of Paintball: Clear Cut",
|
||||
"Tooltip": "Go to ^Paintball.Clearcut and finish 3 games.",
|
||||
"ChallengeId": 2,
|
||||
"Name": "Debug2AIJumbotron",
|
||||
"Config": "{\"ct\":1,\"ipc\":false,\"ctc\":[{\"ct\":0,\"ipc\":false,\"wc\":[{\"ct\":6,\"vs\":[5]},{\"ct\":7,\"vs\":[{\"l\":\"acc06e66-c2d0-4361-b0cd-46246a4c455c\"}]}]}],\"t\":2}",
|
||||
"Description": "Defeat 2 enemies in ^TheRiseOfJumbotron",
|
||||
"Tooltip": "Go to ^TheRiseOfJumbotron and take out 2 enemies.",
|
||||
"Complete": false
|
||||
}
|
||||
],
|
||||
|
||||
Reference in New Issue
Block a user