BREAKING CHANGE: the Go module path is now github.com/tiennm99/tiennm99bot,
the default sticker pack is stickers_by_<bot username>, and the health body,
deploy DM, User-Agents and image names say tiennm99bot.
Under normal load on the shared host, a /wheelofnames GIF takes 11-12s
and a /gacha wish 14-15s to render, so the 15s default turned ordinary
renders into 504s. The default is now 30s, and the bot waits 45s so it
never abandons a render the renderer is still allowed to finish.
This reverts commit 153fc21. With no fallback in code, the deploy
crash-looped: Coolify keeps an entry for every variable compose.yml has
referenced, and empty entries appear to reach the containers as empty
values instead of the compose defaults.
compose.yml now holds each default as ${VAR:-default}, and the code keeps
no fallback values. The bot stops at startup on a missing or invalid
PORT or LOG_LEVEL, /addsticker refuses without STICKER_PACK_NAME, and the
renderer refuses to start until every RENDERER_* setting is a valid
value, listing each problem. Settings whose empty value means none or
all (MODULES, OWNER_ID, ADMIN_IDS, the API tokens) use ${VAR:-}.
The renderer's npm start and dev load .env when present, so a local run
works from a copy of .env.example.
BREAKING CHANGE: running outside compose now requires LOG_LEVEL and PORT
for the bot, STICKER_PACK_NAME for /addsticker, and every RENDERER_*
variable for the renderer.
The renderer's settings now read RENDERER_HOST, RENDERER_PORT,
RENDERER_MAX_CONCURRENT_RENDERS, RENDERER_RENDER_TIMEOUT_MS,
RENDERER_MAX_OPTIONS, and RENDERER_MAX_OPTION_CHARS, so they read as
renderer settings and cannot clash with the bot's variables, which
Coolify injects into every service. NODE_ENV keeps its standard name.
BREAKING CHANGE: the unprefixed HOST, PORT, MAX_CONCURRENT_RENDERS,
RENDER_TIMEOUT_MS, MAX_OPTIONS, and MAX_OPTION_CHARS are no longer read
by the renderer.
The bot now takes the renderer's base URL and appends each /api/<name>
route itself, instead of taking the full /api/gif endpoint and swapping
its last path segment for /gacha and /genshin. The client, its files, and
its errors are named after the renderer rather than wheelofnames; the
/wheelofnames command keeps its name.
BREAKING CHANGE: WHEELOFNAMES_API_URL is replaced by RENDERER_URL, which
holds the base URL (e.g. http://renderer:3000) rather than the /api/gif
endpoint. compose.yml sets it, so Coolify needs no value.
The renderer now runs only on the compose network with no published port
or domain, so a shared bearer token adds nothing. The renderer no longer
checks Authorization or requires API_TOKEN in production, and the bot no
longer sends a token.
BREAKING CHANGE: WHEELOFNAMES_API_TOKEN and the renderer's API_TOKEN are
removed. Never publish the renderer's port: its API is unauthenticated.
The bot now reaches the renderer at http://renderer:3000/api/gif on the
compose network instead of a separately deployed service. Both share
WHEELOFNAMES_API_TOKEN. CI runs the renderer's lint, typecheck, tests,
render smoke, and image build.
Brings back the Genshin-style meteor wish that /api/gacha rendered before pack-cards, under genshin names. It takes the same request as /api/gacha and returns the 7-second landscape MP4.
The sliding rainbow sheen over the whole face and the spectrum foil are
gone. 4★ cards are now engraved with pack-cards' epic contour and 5★ cards
with the legendary rings or facets, and those engraved lines carry a still
rainbow; 3★ cards stay plain.
The warm-up also renders a 4★ wish, since the first card with the contour
engraving otherwise rendered about seven seconds slower.
/api/gacha now renders the 6-second portrait pack-cards opening that was
served on /api/gachabeta. The Remotion meteor wish, its timeline, and the
beta route are removed, and the pack-cards renderer and page take the plain
gacha names.
BREAKING CHANGE: /api/gachabeta is gone, and /api/gacha returns a 6-second
portrait video (360x640 for width 640, 480x854 for width 854) instead of the
7-second landscape one.
/api/gachabeta now returns a portrait video with the requested width as its long edge: 640 renders 360x640 and 854 renders 480x854. The request body is unchanged, and /api/gacha stays landscape. The pack sits higher so it stays in frame while the card rises.
/api/gachabeta now tears open a pack-cards collectible pack whose card
shows the request's label, the same B/A/S rank as /api/gacha, and one
star per rarity level; rarity also picks the card material and the pack
colour. pack-cards animates on the browser clock, so the route drives a
shared headless Chrome in deterministic mode over a debugging pipe,
steps virtual time frame by frame, tears the pack with a scripted drag,
and encodes the captured frames with Remotion's bundled ffmpeg. The
page and package are served from disk with all other requests blocked.
The browser stays alive per process and is warmed at start-up, because
the first render compiles the pack's WebGL shaders in software. The
Remotion beta composition and its scene code are removed. pack-cards is
installed from a GitHub tarball pinned to a commit, since it has no npm
release and a moving URL would break npm ci.
The beta splash card used a fixed SSS rank and six stars. It now uses the
same rarity info as /api/gacha: the rank letter (B, A, or S) on the emblem
and name plate, and one star per rarity level beside the caller's label.
The beta wish now cuts like an anime gacha game's six-star wish over 10.5
seconds: a dive through a painterly cloud vortex, a beam pass, a meteor
that stops inside a rainbow halo and bursts, a red flash, falling crystal
comets joined by a red meteor, a black emblem silhouette shedding shards
on a red disc, and a splash card with a name plate and six stars.
Cloud layers are painted once per roll onto a canvas with a seeded noise
displacement, so frames only move the baked image instead of running a
live SVG filter. The /api/gachabeta contract is unchanged.
The beta wish now renders an 8-second night sky over a snow-rimmed
mountain ridge, where coloured meteors glide down the diagonal. A hero
meteor in the rarity colour slows and burns out at the centre, where the
label appears under rank SSS. The toon cloud, comet, and perspective
camera are removed. The /api/gachabeta contract is unchanged.
The beta wish renders an 8-second astrology-themed night sky in toon
shading through a perspective action camera: it dollies toward a hero
cloud, a comet lights the cloud's rim from behind and bursts through it,
the camera chases the comet as its flare builds to a starburst, and the
label appears with an S, SS, or SSS rank. It shares the /api/gacha request
contract and render slots.
The meteor now flies in from the upper left and lands on the rank emblem,
so the impact burst becomes the badge reveal. The 5-star rainbow ring is
replaced by a rainbow sunburst of counter-rotating rays. Sky stars twinkle
with short bright flares and cross glints, and each roll draws a fresh
layout from a per-request seed that the route picks when the caller sends
none.
Every beat now scales from a per-tier effects table: 4-star adds a bigger
meteor, lens flare, impact shake, and double shockwave; 5-star adds a
rainbow halo before landing, a gold sky flood, a starburst, counter-rotating
rays, falling sparkles, and an emblem sheen. The emblem shows the rank
letter instead of the label's first character.
A meteor coloured by rarity falls across a night sky, lands in a white
flash, and the label is revealed with its stars popping in. The caller
picks the result and rarity; the route only draws it. Renders as silent
H.264 so Telegram plays it as an animation without GIF banding, and
shares the render slots with /api/gif.
Replace pnpm-lock.yaml with package-lock.json. allowBuilds for esbuild becomes
package.json#allowScripts. The Dockerfile installs with npm ci --omit=dev and
no longer prepares pnpm through corepack; .npmrc keeps engine-strict, which npm
honours natively.
minimumReleaseAgeExclude is dropped rather than translated: pnpm's
minimumReleaseAge was never set, so the 30-entry exclusion list had nothing to
exclude from. Direct dependency versions resolve identically to the pnpm
lockfile.
Draw the winner outline as an unfilled pie after every slice: slices paint
in index order, so the next one covered the shared radial edge and the
outline stopped short of wrapping the wedge.
Derive the announcement colors from the winning slice - its own color as
the chip, a deepened version as the frame and the wedge outline - instead
of a fixed dark chip that read as unrelated to the wheel.
Raise the confetti above the announcement so the burst is no longer hidden
behind it, and steepen the launch fan so the field arcs around the name
rather than settling on it: horizontal reach is vx / drag, which parked the
flat fan at mid-canvas. Measured over the hold, particle samples inside the
pill drop from 22.4% to 6.2% while disc coverage holds at 85.5%.
README described confetti as reusing theme slice colors and made no mention of
the winner callout or the reveal timing.
Fill in the benchmark rows that were TBD, and record where the GIF growth
actually came from. Isolating each change shows the celebration rework, pill,
spin retune, palette reorder and pointer together account for +3.3%, while
grayscale label antialiasing accounts for +22.4% on its own because it affects
every spin frame rather than only the celebration. That trade is deliberate:
it removes visible color fringing and stops the palette being exhausted.
Also correct the size levers. Particle count is not one - cutting 43% of the
particles changes the file by 1.1%. Note that render times on this machine vary
more than 2x across repeat runs of identical input, so single timings are
indicative only.