mirror of
https://github.com/tiennm99/tiennm99bot.git
synced 2026-10-14 18:19:22 +00:00
fix(gacha): show the requested rank and stars on the beta reveal
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.
This commit is contained in:
1 parent
081ab00f2e
commit
f75c0e560b
5 files changed
+49
-42
No files matched your search
+5
-5
@@ -77,13 +77,13 @@ gacha game's six-star wish:
|
||||
3. Crystal comets fall through the daytime sky while a red star joins them
|
||||
and strikes in a white flash.
|
||||
4. A black silhouette of the rank emblem sheds shards on a red disc, then the
|
||||
splash card shows the emblem, a name plate with the label and `Rank SSS`,
|
||||
and six stars popping in.
|
||||
splash card shows the emblem, a name plate with the request's `label` and
|
||||
rank, and one star per rarity level popping in.
|
||||
|
||||
The caller randomly chooses the food and sends it as `label` (for example,
|
||||
`"Bún bò"`). The beta reveal always shows `SSS`; `rarity` tints the meteor
|
||||
head and the splash card's emblem (blue 3★, purple 4★, gold 5★). `/api/gacha`
|
||||
keeps its existing ranks.
|
||||
`"Bún bò"`). As on `/api/gacha`, `rarity` picks the rank (`B` for 3★, `A` for
|
||||
4★, `S` for 5★), the star count, and the tint of the meteor head and emblem
|
||||
(blue, purple, gold).
|
||||
|
||||
Response is a silent H.264 `video/mp4` (Telegram plays it as an animation)
|
||||
with `X-Gacha-Rarity` and `X-Render-Duration-Ms` headers. Both routes share the
|
||||
|
||||
@@ -5,7 +5,6 @@ import {bakeCloudLayer} from './gacha-beta-paint.js';
|
||||
import {
|
||||
createShards,
|
||||
gachaBetaTimeline,
|
||||
getBetaRank,
|
||||
getBurstState,
|
||||
getFlash,
|
||||
getHaloState,
|
||||
@@ -19,7 +18,14 @@ import {
|
||||
progress,
|
||||
volleyComets,
|
||||
} from './gacha-beta-scene.js';
|
||||
import {createRandom, createStarfield, getRarityPalette, getTwinkle, starColor} from './gacha-timeline.js';
|
||||
import {
|
||||
createRandom,
|
||||
createStarfield,
|
||||
getRankLetter,
|
||||
getRarityPalette,
|
||||
getTwinkle,
|
||||
starColor,
|
||||
} from './gacha-timeline.js';
|
||||
|
||||
const sansFont =
|
||||
'Quicksand, Inter, "Noto Sans", "Noto Sans Vietnamese", ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif';
|
||||
@@ -149,7 +155,15 @@ const createBetaLayout = (seed, width, height) => {
|
||||
};
|
||||
|
||||
/** @typedef {ReturnType<typeof createBetaLayout>} BetaLayout */
|
||||
/** @typedef {{seconds: number, width: number, height: number, layout: BetaLayout, palette: import('./gacha-timeline.js').RarityPalette}} ShotProps */
|
||||
/**
|
||||
* @typedef {object} ShotProps
|
||||
* @property {number} seconds
|
||||
* @property {number} width
|
||||
* @property {number} height
|
||||
* @property {BetaLayout} layout
|
||||
* @property {import('./gacha-timeline.js').RarityPalette} palette
|
||||
* @property {'B' | 'A' | 'S'} rank Rank for the request's rarity, as on /api/gacha.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Four-point star glint with a soft core, used for meteor heads.
|
||||
@@ -611,9 +625,9 @@ const VolleyShot = ({seconds, width, height, layout}) => {
|
||||
* The rank emblem: a faceted rhombus crystal around the rank letters. Drawn
|
||||
* solid black for the silhouette and in the rarity colours on the card.
|
||||
*
|
||||
* @param {{size: number, silhouette: boolean, palette: import('./gacha-timeline.js').RarityPalette}} props
|
||||
* @param {{size: number, silhouette: boolean, palette: import('./gacha-timeline.js').RarityPalette, rank: string}} props
|
||||
*/
|
||||
const Emblem = ({size, silhouette, palette}) => {
|
||||
const Emblem = ({size, silhouette, palette, rank}) => {
|
||||
const fill = silhouette ? '#050305' : `linear-gradient(135deg, #ffffff 0%, ${palette.emblem} 100%)`;
|
||||
return (
|
||||
<div style={{height: size, position: 'relative', width: size}}>
|
||||
@@ -651,14 +665,14 @@ const Emblem = ({size, silhouette, palette}) => {
|
||||
textShadow: silhouette ? undefined : `0 0 ${size * 0.06}px ${palette.glow}`,
|
||||
}}
|
||||
>
|
||||
{getBetaRank()}
|
||||
{rank}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
/** @param {ShotProps} props */
|
||||
const SilhouetteShot = ({seconds, width, height, layout, palette}) => {
|
||||
const SilhouetteShot = ({seconds, width, height, layout, palette, rank}) => {
|
||||
const tl = gachaBetaTimeline;
|
||||
const t = progress(seconds, tl.volleyEnd, tl.silhouetteEnd);
|
||||
const cx = width / 2;
|
||||
@@ -741,16 +755,16 @@ const SilhouetteShot = ({seconds, width, height, layout, palette}) => {
|
||||
top: cy - emblem / 2,
|
||||
}}
|
||||
>
|
||||
<Emblem palette={palette} silhouette size={emblem} />
|
||||
<Emblem palette={palette} rank={rank} silhouette size={emblem} />
|
||||
</div>
|
||||
</AbsoluteFill>
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* @param {{seconds: number, width: number, height: number, layout: BetaLayout, palette: import('./gacha-timeline.js').RarityPalette, label: string}} props
|
||||
* @param {ShotProps & {label: string, rarity: import('./gacha-timeline.js').GachaRarity}} props
|
||||
*/
|
||||
const RevealShot = ({seconds, width, height, layout, palette, label}) => {
|
||||
const RevealShot = ({seconds, width, height, layout, palette, rank, label, rarity}) => {
|
||||
const tl = gachaBetaTimeline;
|
||||
const settle = progress(seconds, tl.silhouetteEnd, gachaBetaTimeline.silhouetteEnd + 2.5);
|
||||
const plateIn = interpolate(seconds, [tl.plateIn, tl.plateIn + 0.4], [0, 1], {
|
||||
@@ -828,7 +842,7 @@ const RevealShot = ({seconds, width, height, layout, palette, label}) => {
|
||||
top: height * 0.46 - emblem / 2 + Math.sin(seconds * 1.5) * height * 0.008,
|
||||
}}
|
||||
>
|
||||
<Emblem palette={palette} silhouette={false} size={emblem} />
|
||||
<Emblem palette={palette} rank={rank} silhouette={false} size={emblem} />
|
||||
</div>
|
||||
</AbsoluteFill>
|
||||
<div
|
||||
@@ -873,7 +887,7 @@ const RevealShot = ({seconds, width, height, layout, palette, label}) => {
|
||||
textShadow: `0 ${height * 0.004}px ${height * 0.012}px rgba(0,0,0,0.7)`,
|
||||
}}
|
||||
>
|
||||
{`Rank ${getBetaRank()}`}
|
||||
{`Rank ${rank}`}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -885,7 +899,7 @@ const RevealShot = ({seconds, width, height, layout, palette, label}) => {
|
||||
marginTop: height * 0.012,
|
||||
}}
|
||||
>
|
||||
{getStarRevealTimes().map((time, index) => {
|
||||
{getStarRevealTimes(rarity).map((time, index) => {
|
||||
const pop = interpolate(seconds, [time, time + 0.12, time + 0.24], [0, 1.4, 1], clamp);
|
||||
return (
|
||||
<div
|
||||
@@ -925,7 +939,7 @@ export const GachaBetaComposition = (props) => {
|
||||
const palette = getRarityPalette(props.rarity);
|
||||
const shot = getShot(seconds);
|
||||
const flash = getFlash(seconds);
|
||||
const shared = {seconds, width, height, layout, palette};
|
||||
const shared = {seconds, width, height, layout, palette, rank: getRankLetter(props.rarity)};
|
||||
|
||||
return (
|
||||
<AbsoluteFill
|
||||
@@ -941,7 +955,7 @@ export const GachaBetaComposition = (props) => {
|
||||
{shot === 'sky' ? <SkyShot {...shared} /> : null}
|
||||
{shot === 'volley' ? <VolleyShot {...shared} /> : null}
|
||||
{shot === 'silhouette' ? <SilhouetteShot {...shared} /> : null}
|
||||
{shot === 'reveal' ? <RevealShot {...shared} label={props.label} /> : null}
|
||||
{shot === 'reveal' ? <RevealShot {...shared} label={props.label} rarity={props.rarity} /> : null}
|
||||
{flash.opacity > 0 ? <AbsoluteFill style={{background: flash.color, opacity: flash.opacity}} /> : null}
|
||||
{flash.opacity > 0 && flash.color !== '#ffffff' && shot === 'sky' ? (
|
||||
<AbsoluteFill
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
* pass, a meteor that slows over a sea of clouds inside a rainbow halo and
|
||||
* bursts, a red flash, a volley of falling crystal comets joined by a red
|
||||
* one, a black silhouette on a red disc that sheds shards, and the splash
|
||||
* card with its name plate and six stars. Kept free of React so the
|
||||
* card with its name plate, rank, and stars. Kept free of React so the
|
||||
* choreography can be unit tested.
|
||||
*/
|
||||
|
||||
@@ -26,8 +26,7 @@ export const gachaBetaTimeline = Object.freeze({
|
||||
starStep: 0.14,
|
||||
});
|
||||
|
||||
/** The beta always reveals six stars and rank SSS. */
|
||||
export const betaStarCount = 6;
|
||||
/** @typedef {import('./gacha-timeline.js').GachaRarity} GachaRarity */
|
||||
|
||||
/** @typedef {'vortex' | 'beam' | 'sky' | 'volley' | 'silhouette' | 'reveal'} BetaShot */
|
||||
|
||||
@@ -273,12 +272,11 @@ export const getShardOffset = (shard, seconds) => {
|
||||
};
|
||||
|
||||
/**
|
||||
* Seconds at which each reveal star pops in, left to right.
|
||||
* Seconds at which each reveal star pops in, left to right: one star per
|
||||
* rarity level.
|
||||
*
|
||||
* @param {GachaRarity} rarity
|
||||
* @returns {number[]}
|
||||
*/
|
||||
export const getStarRevealTimes = () =>
|
||||
Array.from({length: betaStarCount}, (_, index) => gachaBetaTimeline.starsStart + index * gachaBetaTimeline.starStep);
|
||||
|
||||
/** The beta food wish always reveals SSS; rarity tints the meteor and splash. */
|
||||
export const getBetaRank = () => 'SSS';
|
||||
export const getStarRevealTimes = (rarity) =>
|
||||
Array.from({length: rarity}, (_, index) => gachaBetaTimeline.starsStart + index * gachaBetaTimeline.starStep);
|
||||
@@ -24,5 +24,5 @@ const createGachaRenderer = (compositionId) => (inputProps, options) =>
|
||||
|
||||
export const renderGachaVideo = createGachaRenderer('GachaWish');
|
||||
|
||||
/** The beta wish: a six-star style wish sequence ending in an SSS splash card. */
|
||||
/** The beta wish: a six-star style wish sequence ending in a ranked splash card. */
|
||||
export const renderGachaBetaVideo = createGachaRenderer('GachaBetaWish');
|
||||
@@ -1,10 +1,8 @@
|
||||
import {describe, expect, test} from 'vitest';
|
||||
import {
|
||||
betaStarCount,
|
||||
createShards,
|
||||
gachaBetaTimeline,
|
||||
gachaBetaTotalSeconds,
|
||||
getBetaRank,
|
||||
getFlash,
|
||||
getHaloState,
|
||||
getMeteorHead,
|
||||
@@ -35,12 +33,13 @@ describe('gacha beta wish scene', () => {
|
||||
expect(tl.redStarIn).toBeLessThan(tl.volleyEnd);
|
||||
});
|
||||
|
||||
test('the six stars finish popping with time to hold the card', () => {
|
||||
const times = getStarRevealTimes();
|
||||
expect(times).toHaveLength(betaStarCount);
|
||||
expect(betaStarCount).toBe(6);
|
||||
expect(times[0]).toBeGreaterThan(gachaBetaTimeline.plateIn);
|
||||
expect((times.at(-1) ?? Infinity) + 1).toBeLessThan(gachaBetaTotalSeconds);
|
||||
test('one star per rarity level, finishing with time to hold the card', () => {
|
||||
for (const rarity of /** @type {const} */ ([3, 4, 5])) {
|
||||
const times = getStarRevealTimes(rarity);
|
||||
expect(times).toHaveLength(rarity);
|
||||
expect(times[0]).toBeGreaterThan(gachaBetaTimeline.plateIn);
|
||||
expect((times.at(-1) ?? Infinity) + 1).toBeLessThan(gachaBetaTotalSeconds);
|
||||
}
|
||||
});
|
||||
|
||||
test('flashes open the clip, cover the burst in red, and hand over at each loud cut', () => {
|
||||
@@ -102,8 +101,4 @@ describe('gacha beta wish scene', () => {
|
||||
expect(Math.hypot(end.dx, end.dy)).toBeCloseTo(shard.reach);
|
||||
}
|
||||
});
|
||||
|
||||
test('beta food reveal displays SSS', () => {
|
||||
expect(getBetaRank()).toBe('SSS');
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user