mirror of
https://github.com/tiennm99/tiennm99bot.git
synced 2026-10-11 12:28:54 +00:00
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.
111 lines
4.3 KiB
JavaScript
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));
|
|
});
|
|
});
|