diff --git a/renderer/README.md b/renderer/README.md index 92187a1..b101928 100644 --- a/renderer/README.md +++ b/renderer/README.md @@ -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 diff --git a/renderer/src/remotion/GachaBetaComposition.jsx b/renderer/src/remotion/GachaBetaComposition.jsx index 3aecb24..000145c 100644 --- a/renderer/src/remotion/GachaBetaComposition.jsx +++ b/renderer/src/remotion/GachaBetaComposition.jsx @@ -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} 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 (
@@ -651,14 +665,14 @@ const Emblem = ({size, silhouette, palette}) => { textShadow: silhouette ? undefined : `0 0 ${size * 0.06}px ${palette.glow}`, }} > - {getBetaRank()} + {rank}
); }; /** @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, }} > - + ); }; /** - * @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, }} > - +
{ textShadow: `0 ${height * 0.004}px ${height * 0.012}px rgba(0,0,0,0.7)`, }} > - {`Rank ${getBetaRank()}`} + {`Rank ${rank}`}
@@ -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 (
{ 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 ( { {shot === 'sky' ? : null} {shot === 'volley' ? : null} {shot === 'silhouette' ? : null} - {shot === 'reveal' ? : null} + {shot === 'reveal' ? : null} {flash.opacity > 0 ? : null} {flash.opacity > 0 && flash.color !== '#ffffff' && shot === 'sky' ? ( { }; /** - * 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); diff --git a/renderer/src/render/render-gacha.js b/renderer/src/render/render-gacha.js index 89750d0..477d55a 100644 --- a/renderer/src/render/render-gacha.js +++ b/renderer/src/render/render-gacha.js @@ -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'); diff --git a/renderer/test/gacha-beta-scene.test.js b/renderer/test/gacha-beta-scene.test.js index 91e71aa..63c573a 100644 --- a/renderer/test/gacha-beta-scene.test.js +++ b/renderer/test/gacha-beta-scene.test.js @@ -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'); - }); });