Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 6 additions & 5 deletions docs/CONTRACTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ everything else is revalidated.
| GET | `/api/catalog/:channel` | none | none | `[...static characters.json, ...custom entries]` (section 5) | 403 |
| GET | `/api/profile/:channel` | cookie | none | `Profile & {owned:{pets:[id], hats:[id], recolor:[id], petcolor:[id], accessory:[id], trail:[id], effect:[id], taunt:[id], title:[id], slots:n}, builds:[Loadout\|null]}` or `null`. `slots` is 1 free plus the bought ones; `builds[i]` is slot i's saved loadout (`avatar, color, defaultAbility, stats, hat, pet` and the 7 cosmetic fields), `null` if never saved; the active slot is always the profile itself | 401 |
| GET | `/api/profile/:channel/others` | cookie | none | `{fighters:[{channel, avatar, color, defaultAbility, lastSeen}]}`: the viewer's saved fighters on other channels that are on or paused, newest `lastSeen` first. It checks the channels in AuthStore `fighters:<userId>` plus the built-in ones, at most 5. Only the base look; gear stays on its channel. The fighter page offers these as "Use my <channel> fighter" when the viewer has no fighter on this channel | 401, 404 any other subpath or method |
| POST | `/api/profile/:channel` | cookie | `{avatar, color:"#rrggbb", defaultAbility:"strike"\|"heavy"\|"heal", stats?:{power,guard,luck}, hat?, pet?, recolor?, petColor?, accessory?, trail?, winEffect?, taunt?, title?, build?}`, max 4000 bytes. `stats` are checked against the points from shown wins (a duel result the stream hasn't played yet doesn't count) plus check-ins, `hat` against the shown wins or a bought hat (server/upgrades.ts), and `pet` (`""` for none, at most 64 chars) against the pets the fighter owns (`pet_locked`); a change of stats or pet boost while the fighter is in a duel is refused (`in_duel`). Leaving `pet` out keeps the saved pet. Each cosmetic field is `""` (none) or an id from `/api/shop` `items` that the fighter bought (`item_locked`; the one already worn is kept); left out keeps the saved one. `build` (an integer 0 to 4, else a plain 400; a slot not bought yet is `invalid_build`) is the slot the loadout is saved to, and it becomes the active one; left out = the active slot. A save never changes Elo, wins, losses or the last opponent: a fighter off the active list (10 quiet minutes) rejoins with the ones in its saved profile. | `{profile, revision}`; `profile` carries `upgrades`, `owned` and `builds` as in the GET (a build saved with an uploaded pet deleted since comes back with `pet:""`) | 400 invalid field or unknown character, 401, 403 "That sprite belongs to another viewer" (a viewer sprite with another `owner`), 413 |
| POST | `/api/profile/:channel` | cookie | `{avatar, color:"#rrggbb", defaultAbility:"strike"\|"heavy"\|"heal", stats?:{power,guard,luck,crit,dodge,counter}, hat?, pet?, recolor?, petColor?, accessory?, trail?, winEffect?, taunt?, title?, build?}`, max 4000 bytes. `stats` are checked against the points from shown wins (a duel result the stream hasn't played yet doesn't count) plus check-ins, `hat` against the shown wins or a bought hat (server/upgrades.ts), and `pet` (`""` for none, at most 64 chars) against the pets the fighter owns (`pet_locked`); a change of stats or pet boost while the fighter is in a duel is refused (`in_duel`). Leaving `pet` out keeps the saved pet. Each cosmetic field is `""` (none) or an id from `/api/shop` `items` that the fighter bought (`item_locked`; the one already worn is kept); left out keeps the saved one. `build` (an integer 0 to 4, else a plain 400; a slot not bought yet is `invalid_build`) is the slot the loadout is saved to, and it becomes the active one; left out = the active slot. A save never changes Elo, wins, losses or the last opponent: a fighter off the active list (10 quiet minutes) rejoins with the ones in its saved profile. | `{profile, revision}`; `profile` carries `upgrades`, `owned` and `builds` as in the GET (a build saved with an uploaded pet deleted since comes back with `pet:""`) | 400 invalid field or unknown character, 401, 403 "That sprite belongs to another viewer" (a viewer sprite with another `owner`), 413 |
| GET | `/api/sprite/:channel` | cookie | none | The viewer's own sprite: `{live, pending, rejected, submitsLeft, aiLeft, ai:{available, left}, limits}`. `live` has `url`; each is `{id, label, status, createdAt, ...}` or `null`. `ai.available` is false where the Worker has no `AI` binding (local dev, tests) | 401, 403 channel off |
| GET | `/api/sprite/:channel/pending` | cookie | none | The viewer's waiting sprite PNG (`private, no-store`) | 401, 404 |
| POST | `/api/sprite/:channel` | cookie | `{label:1-24 chars, image:<base64 PNG, data: prefix allowed>, ai?:bool}`; at most 128 by 128 px and 64 KB. The page makes the PNG (src/spritify.ts). The Worker forwards the session's id and name, never ones from the body | 201 `{ok:true, pending}`. Replaces the viewer's waiting or turned-down sprite | 400 `invalid_label` (also a label that is not text), `invalid_image` (also an image that is not text), `invalid_upload` (`ai` not a boolean), `image_dimensions`, 413 `image_too_large`, 415 `not_png`, 401, 409 `queue_full` (40 waiting per channel), 429 `daily_limit` (6 sends per viewer per UTC day) |
Expand Down Expand Up @@ -249,7 +249,7 @@ Event types and their fields:
| `challenge_declined` | duelId, declinedBy |
| `challenge_expired` | duelId, a, b |
| `duel_started` | duelId, a, b, round, hp |
| `duel_action` | duelId, userId, targetId (the actor for heal), ability, amount, hp. Also sent for the final blow. A quick-duel roll adds die (1-6) and one of miss, crit or counter (userId is then the defender who counters), and finisher on the last blow. |
| `duel_action` | duelId, userId, targetId (the actor for heal), ability, amount, hp. Also sent for the final blow. A quick-duel roll adds die (1-6) and one of miss, crit or counter (userId is then the defender who counters), and finisher on the last blow; upgrades add lucky (luck turned a miss into a hit), luckyCrit (crit turned a hit into a crit) and dodged (the defender's dodge turned a hit or crit into a miss, which a counter may follow). An HP-fight strike or heavy that crits adds crit. |
| `duel_completed` | duelId, winnerId, loserId, round, respawnAt, hp, ratings. Quick duels may add flawless (ratings of the winner then include bonus) and decision (`hp` or `sudden_death`); an unrated duel adds `unrated` (`new_account` or `pair_cap`). |
| `duel_cancelled` | duelId, a, b, reason (`inactivity`, `chat_disconnected`, `duels_disabled`, `moderator_cancelled`, `moderator_reset`, `player_removed`), wasActive |
| `player_respawned` | userId |
Expand Down Expand Up @@ -433,7 +433,8 @@ Viewer sprites (server/sprites.ts):
challenge expires. Either side may `!decline`. Challenging someone who already challenged you accepts their challenge.
- A player can be in at most 1 open duel, and the channel holds at most `maxDuels` open duels.
- Quick duels are the default (`quickDuel:true`): accepting settles the duel at once (see the quick duel rule below). The HP fight described next runs only when a config sets `quickDuel:false`; no screen does that, so it takes an admin config call. StreamElements has no attack commands, so its duels are always quick.
- HP fight abilities: `strike` and `heavy` deal `damage`; `heal` restores `amount`, capped at maxHp. Each
- HP fight abilities: `strike` and `heavy` deal `damage` (+50% on a crit, rolled with the actor's crit chance); `heal`
restores `amount`, capped at maxHp. Each
ability has its own `cooldownMs`, plus a shared cooldown of `sharedCooldownMs` after any action.
- The first player to reach 0 hp loses. The winner returns to maxHp. The loser is KO'd until
`respawnAt = now + respawnMs`. Elo uses K=`eloK` from `initialElo`. The same pair cannot duel again
Expand All @@ -449,10 +450,10 @@ Viewer sprites (server/sprites.ts):
between them in 24 h already): no Elo or dollars."
- An active duel with no action for `inactivityMs` is cancelled without scoring. Cancelled duels
return both players to maxHp.
- Upgrades (server/upgrades.ts): every win earns one point and check-ins add more (`bonus`), with no cap. Points buy levels in power (damage dealt), guard (damage taken) and luck (the chance a quick-duel miss lands as a hit). Levels 1-8 cost 1 point each and give 4% each; the cost doubles every 8 levels (9-16 cost 2, 17-24 cost 4), and above level 8 the effect closes in on a ceiling (power and guard 60%, luck 50%; level 16 gives about 44.2%). Stored levels stop at 200. The channel's `maxStatLevel` caps the levels that count in its duels; levels above it stay saved. A rank reset that leaves too few points takes levels off luck, then guard, then power. Profiles carry `bonus` (check-in points), `checkins` and `streak`; the profile answer's `upgrades` has `points`, `fromWins`, `fromCheckins` and `maxLevel`. The fighter page offers suggested builds (`PRESETS`) that fill the levels from the points; the viewer still saves. Points can be moved while the fighter is not in a duel. Hats are cosmetic; some unlock at a number of wins. A rank reset clears Elo and wins but keeps `bonus`.
- Upgrades (server/upgrades.ts): every win earns one point and check-ins add more (`bonus`), with no cap. Points buy levels in power (damage dealt), guard (damage taken), luck (the chance a quick-duel miss lands as a hit), crit (the chance a hit becomes a crit; in an HP fight, that a strike or heavy deals +50%), dodge (the chance a quick-duel hit or crit on you misses) and counter (quick duels: +counter damage, and the chance a foe's miss becomes your counter). Levels 1-8 cost 1 point each and give 4% each (counter: 2% damage and 2% chance); the cost doubles every 8 levels (9-16 cost 2, 17-24 cost 4), and above level 8 the effect closes in on a ceiling (power, guard and counter damage 60%, luck, crit, dodge and counter chance 50%; a 4% stat at level 16 gives about 44.2%). A quick-duel swing checks, in order and each with its own roll (`rolls[k * 24 + i]`, slot k: die 0, luck 1, crit 2, dodge 3, counter 4; the room sends 120 rolls): the attacker's luck, the attacker's crit, the defender's dodge, the defender's counter. Stored levels stop at 200. The channel's `maxStatLevel` caps the levels that count in its duels; levels above it stay saved. A rank reset that leaves too few points takes levels off counter, then dodge, crit, luck, guard and power. Profiles carry `bonus` (check-in points), `checkins` and `streak`; the profile answer's `upgrades` has `points`, `fromWins`, `fromCheckins` and `maxLevel`. The fighter page offers suggested builds (`PRESETS`) that fill the levels from the points; the viewer still saves. Points can be moved while the fighter is not in a duel. Hats are cosmetic; some unlock at a number of wins. A rank reset clears Elo and wins but keeps `bonus`.
- Dollars: every finished ranked duel pays `winDollars` to the winner and `lossDollars` to the loser, saved fighters only. game.ts records it as `duel.payout` and the room adds it to `profiles.dollars` once, in the same transaction (`duel.paid`). Like Elo, a payout stays out of `!wallet`, `/api/profile` and the leaderboard until the stream has shown the fight. Profiles carry `dollars` (at most 1,000,000). Check-ins give points, not dollars. A rank reset, a profile save and a re-save keep dollars; removing a viewer deletes them.
- `!wallet` (the StreamElements `wallet` action, also while duels are paused): "@x: $N PixFray dollars, P upgrade points, S-stream streak." Reasons: `wallet`, `no_fighter`.
- Pets and the shop (server/pets.ts): 14 built-in pets in 5 tiers, plus up to 24 uploaded per channel. A fighter owns any number of pets (`owned_items`) and brings one (`profiles.pet`); hats can be bought too. A pet's boost is added after the channel's `maxStatLevel` cap: common and uncommon +1 to one stat, rare +2, epic +2 and +1 to a second stat, legendary +1 to every stat. Pets cost `petPrice<Tier>` dollars; a hat whose wins aren't reached costs `hatPricePerWin` times the wins it needs. The overlay draws the pet behind its fighter at about 40% of its height; epic and legendary pets sparkle, legendary pets hover. Snapshot players and `/api/looks` carry `pet` and `petTier`; players also carry `petBoost`.
- Pets and the shop (server/pets.ts): 14 built-in pets in 5 tiers, plus up to 24 uploaded per channel. A fighter owns any number of pets (`owned_items`) and brings one (`profiles.pet`); hats can be bought too. A pet's boost is added after the channel's `maxStatLevel` cap: common and uncommon +1 to one stat, rare +2, epic +2 and +1 to a second stat, legendary +1 to power, guard and luck. Pet boosts use only those three stats. Pets cost `petPrice<Tier>` dollars; a hat whose wins aren't reached costs `hatPricePerWin` times the wins it needs. The overlay draws the pet behind its fighter at about 40% of its height; epic and legendary pets sparkle, legendary pets hover. Snapshot players and `/api/looks` carry `pet` and `petTier`; players also carry `petBoost`.
- Cosmetics and builds (server/cosmetics.ts, drawn by src/served/cosmetics.ts): recolor (a CSS filter on the fighter), pet color (the same 8 tints on the pet), accessories, walking trails, win effects (about 3 s over the winner when the overlay shows the result), win taunts (a speech bubble then) and titles (under the nameplate). Each kind has one config price. Owned cosmetics are `owned_items` rows; the worn ones are profile columns. Build slots: 1 free, the 2nd costs `buildSlotPrice`, the 3rd to 5th `buildSlotPriceMore` each. Each slot keeps its own loadout in the `builds` table; saving to a slot makes it the one on stream, and every build can spend the full point pool. Removing a viewer deletes their builds; a rank reset keeps them.
- `!pet [@name]` (the `pet` action): "@x's pet: Fox (rare), +2 power." Reasons: `pet`, `no_pet` (with the site link for the sender's own), `no_fighter`.
- `!pay @name amount` (the `give` action; on a channel the PixFray bot reads, it's `!give` and `!pay` stays an alias; `a=` is `20` or `$20`): moves dollars between two saved fighters while the channel is live (the same Twitch check as check-ins). In order it refuses with `give_off` (config `giveEnabled` false), `no_fighter`, `give_usage` (no target or no whole amount of at least 1), `target_not_found`, `self_give`, `twitch_error`, `not_live`, `too_few_duels` (fewer than `giveMinDuels` finished duels, `{duels, need}`), `give_cap` (already gave `giveMaxPerStream` this stream), `over_cap` (`{left}`), `not_enough` (`{dollars}`) and `target_full` (`{to, room}`: the receiver's wallet can't hold the amount, so nothing moves). Hidden duel results and payouts don't count yet. The limit is per Twitch stream id (`profiles.give_stream`, `given_in_stream`). Success is `given` with `{amount, to, dollars, left}`; the command log stores the amount.
Expand Down
14 changes: 13 additions & 1 deletion docs/DUELS.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,19 @@ The duel is rolled at once, the moment the challenge is accepted.

## Upgrades, hats and check-ins

- Each win earns an upgrade point (power, guard, luck) and unlocks hats. Points have no cap.
- Each win earns an upgrade point and unlocks hats. Points have no cap. Points buy levels in six stats:

| Stat | Each level (levels 1-8) | Ceiling |
|---|---|---|
| Power | +4% damage dealt | 60% |
| Guard | -4% damage taken | 60% |
| Luck | 4% of your misses still hit | 50% |
| Crit | 4% of your hits become crits (HP fights: a strike or heavy deals +50%) | 50% |
| Dodge | 4% of hits and crits on you miss | 50% |
| Counter | +2% counter damage, and 2% of a foe's misses become your counter | 60% damage, 50% chance |

- Luck, dodge and counter work in quick duels only. A swing checks luck, then crit, then dodge, then
counter, so a dodged hit can still turn into your counter.
- Levels 1-8 of a stat cost 1 point each; the cost doubles every 8 levels after that, and each level
adds less as the stat nears its ceiling. Mods set the highest level that counts on their channel
(16 by default); levels above it stay saved.
Expand Down
2 changes: 2 additions & 0 deletions docs/STREAMER_SETUP.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,8 @@ it, click **Connect mod access** and approve the permission while signed in to T
Moderators can edit chat commands, use Clear arena, change the balance and review viewer sprites.
The Rules tab's Check-ins group has **Highest stat level that counts** (16 by default): fighters' upgrade levels above it
stay saved but don't count in your channel's duels.
Viewers spend their points on six stats (power, guard, luck, crit, dodge and counter) on their
fighter page; docs/DUELS.md lists what each one does.
Only you (or the site owner) can click **Reset all ranks**, **Turn PixFray off** or move chat from
another site.

Expand Down
Loading
Loading