Files
tiennm99bot/renderer/test/gacha-timeline.test.js
T
tiennm99 dccd524f9d feat(gacha): fly into the rank emblem with a rainbow sunburst and twinkling sky
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.
2026-10-01 14:43:46 +07:00

111 lines
4.3 KiB
JavaScript

import {describe, expect, test} from 'vitest';
import {
createStarfield,
gachaTimeline,
gachaTotalSeconds,
getLabelFontSize,
emblemCenter,
getMeteorPoint,
getRankLetter,
getRarityPalette,
getShakeOffset,
getStarRevealTimes,
getTierEffects,
getTwinkle,
} from '../src/remotion/gacha-timeline.js';
describe('gacha timeline', () => {
test('phases run in order and finish before the clip ends', () => {
const tl = gachaTimeline;
expect(tl.meteorStart).toBeLessThan(tl.meteorEnd);
expect(tl.flashStart).toBeLessThan(tl.flashPeak);
expect(tl.meteorEnd).toBeLessThanOrEqual(tl.flashPeak);
expect(tl.flashPeak).toBeLessThan(tl.flashHoldEnd);
expect(tl.revealStart).toBeLessThanOrEqual(tl.flashHoldEnd);
expect(tl.flashHoldEnd).toBeLessThan(tl.flashEnd);
expect(tl.nameIn).toBeLessThan(tl.nameSettled);
const lastStar = getStarRevealTimes(5).at(-1) ?? 0;
expect(lastStar + 1).toBeLessThan(gachaTotalSeconds);
});
test.each([3, 4, 5])('reveals one star per rarity level for %i stars', (rarity) => {
const times = getStarRevealTimes(/** @type {3 | 4 | 5} */ (rarity));
expect(times).toHaveLength(rarity);
expect(times[0]).toBe(gachaTimeline.starsStart);
expect([...times].sort((a, b) => a - b)).toEqual(times);
});
test('each rarity has a distinct meteor colour', () => {
const glows = [3, 4, 5].map((rarity) => getRarityPalette(/** @type {3 | 4 | 5} */ (rarity)).glow);
expect(new Set(glows).size).toBe(3);
});
test('meteor flies in from off-screen upper left and lands on the rank emblem', () => {
const start = getMeteorPoint(0, 640, 360);
const end = getMeteorPoint(1, 640, 360);
expect(start.x).toBeLessThan(0);
expect(start.y).toBeLessThan(0);
expect(end.x).toBeCloseTo(640 * emblemCenter.x);
expect(end.y).toBeCloseTo(360 * emblemCenter.y);
expect(getMeteorPoint(2, 640, 360)).toEqual(end);
expect(getMeteorPoint(-1, 640, 360)).toEqual(start);
});
test('starfield is deterministic per seed', () => {
expect(createStarfield(5, 7)).toEqual(createStarfield(5, 7));
expect(createStarfield(5, 7)).not.toEqual(createStarfield(5, 8));
});
test('stars twinkle with short bright flares', () => {
const [star] = createStarfield(1, 3);
if (!star) {
throw new Error('expected a star');
}
const samples = Array.from({length: 200}, (_, index) => getTwinkle(index / 50, star));
expect(Math.max(...samples)).toBeGreaterThan(0.95);
expect(Math.min(...samples)).toBeLessThan(0.05);
const bright = samples.filter((value) => value > 0.5).length / samples.length;
expect(bright).toBeLessThan(0.4);
});
test('long labels get smaller text', () => {
expect(getLabelFontSize('Pizza', 360)).toBeGreaterThan(getLabelFontSize('x'.repeat(40), 360));
});
test('emblem shows B, A, S rank letters by rarity', () => {
expect([3, 4, 5].map((rarity) => getRankLetter(/** @type {3 | 4 | 5} */ (rarity)))).toEqual(['B', 'A', 'S']);
});
test('every numeric effect grows with rarity and 5★ unlocks the extras', () => {
const [three, four, five] = [3, 4, 5].map((rarity) => getTierEffects(/** @type {3 | 4 | 5} */ (rarity)));
for (const key of /** @type {const} */ ([
'headScale',
'trailSamples',
'sparks',
'shockwaves',
'raysOpacity',
'motes',
'starFlare',
])) {
expect(four?.[key], key).toBeGreaterThan(three?.[key] ?? Infinity);
expect(five?.[key], key).toBeGreaterThan(four?.[key] ?? Infinity);
}
expect(four?.shake).toBeGreaterThan(three?.shake ?? Infinity);
expect(five?.shake).toBeGreaterThan(four?.shake ?? Infinity);
expect(five?.skyFlood).toBeGreaterThan(four?.skyFlood ?? Infinity);
expect([three?.halo, four?.halo, five?.halo]).toEqual([false, false, true]);
expect(five).toMatchObject({starburst: true, counterRays: true, sheen: true});
expect(three?.sparkleRain).toBe(0);
expect(five?.sparkleRain).toBeGreaterThan(0);
});
test('impact shake decays to rest', () => {
expect(getShakeOffset(1, 2, 10)).toEqual({x: 0, y: 0});
expect(getShakeOffset(3, 2, 10)).toEqual({x: 0, y: 0});
expect(getShakeOffset(2.1, 2, 0)).toEqual({x: 0, y: 0});
const early = getShakeOffset(2.05, 2, 10);
const late = getShakeOffset(2.7, 2, 10);
expect(Math.hypot(early.x, early.y)).toBeGreaterThan(Math.hypot(late.x, late.y));
});
});