mirror of
https://github.com/tiennm99/tiennm99bot.git
synced 2026-10-20 02:18:23 +00:00
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.
This commit is contained in:
1 parent
9b93c53647
commit
dccd524f9d
10 files changed
+251
-65
No files matched your search
@@ -5,13 +5,17 @@ const limits = {maxOptionChars: 12};
|
||||
|
||||
describe('parseGachaRequest', () => {
|
||||
test('trims the label and applies defaults', () => {
|
||||
const request = parseGachaRequest({label: ' Pizza ', rarity: 5}, limits);
|
||||
const request = parseGachaRequest({label: ' Pizza ', rarity: 5}, limits, () => 42);
|
||||
|
||||
expect(request).toEqual({label: 'Pizza', rarity: 5, fps: 24, width: 640});
|
||||
expect(request).toEqual({label: 'Pizza', rarity: 5, fps: 24, width: 640, seed: 42});
|
||||
});
|
||||
|
||||
test('keeps a caller-provided seed', () => {
|
||||
expect(parseGachaRequest({label: 'Pizza', rarity: 5, seed: 9}, limits, () => 42).seed).toBe(9);
|
||||
});
|
||||
|
||||
test('accepts the larger frame and fps', () => {
|
||||
const request = parseGachaRequest({label: 'Pho', rarity: 4, fps: 30, width: 854}, limits);
|
||||
const request = parseGachaRequest({label: 'Pho', rarity: 4, fps: 30, width: 854}, limits, () => 1);
|
||||
|
||||
expect(request.fps).toBe(30);
|
||||
expect(request.width).toBe(854);
|
||||
@@ -26,7 +30,9 @@ describe('parseGachaRequest', () => {
|
||||
{label: 'Pizza', rarity: 3, width: 512},
|
||||
{label: 'Pizza', rarity: 3, fps: 15},
|
||||
{label: 'Pizza', rarity: 3, extra: true},
|
||||
{label: 'Pizza', rarity: 3, seed: -1},
|
||||
{label: 'Pizza', rarity: 3, seed: 1.5},
|
||||
])('rejects %o', (input) => {
|
||||
expect(() => parseGachaRequest(input, limits)).toThrow();
|
||||
expect(() => parseGachaRequest(input, limits, () => 0)).toThrow();
|
||||
});
|
||||
});
|
||||
@@ -47,7 +47,32 @@ describe('POST /api/gacha', () => {
|
||||
expect(response.headers['content-type']).toBe('video/mp4');
|
||||
expect(response.headers['x-gacha-rarity']).toBe('5');
|
||||
expect(response.rawPayload.subarray(4, 8).toString()).toBe('ftyp');
|
||||
expect(rendered).toEqual({label: 'Pizza', rarity: 5, fps: 24, width: 640});
|
||||
expect(rendered).toMatchObject({label: 'Pizza', rarity: 5, fps: 24, width: 640});
|
||||
expect(Number.isInteger(/** @type {{seed: number}} */ (rendered).seed)).toBe(true);
|
||||
|
||||
await app.close();
|
||||
});
|
||||
|
||||
test('picks a different seed per roll unless one is given', async () => {
|
||||
/** @type {number[]} */
|
||||
const seeds = [];
|
||||
const app = await build(
|
||||
{},
|
||||
{
|
||||
renderGacha: async (request) => {
|
||||
seeds.push(request.seed);
|
||||
return {buffer: mp4, byteLength: mp4.byteLength, durationMs: 9};
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
for (let index = 0; index < 3; index += 1) {
|
||||
await app.inject({method: 'POST', url: '/api/gacha', payload: {label: 'Pizza', rarity: 5}});
|
||||
}
|
||||
await app.inject({method: 'POST', url: '/api/gacha', payload: {label: 'Pizza', rarity: 5, seed: 5}});
|
||||
|
||||
expect(new Set(seeds.slice(0, 3)).size).toBeGreaterThan(1);
|
||||
expect(seeds[3]).toBe(5);
|
||||
|
||||
await app.close();
|
||||
});
|
||||
|
||||
@@ -4,12 +4,14 @@ import {
|
||||
gachaTimeline,
|
||||
gachaTotalSeconds,
|
||||
getLabelFontSize,
|
||||
emblemCenter,
|
||||
getMeteorPoint,
|
||||
getRankLetter,
|
||||
getRarityPalette,
|
||||
getShakeOffset,
|
||||
getStarRevealTimes,
|
||||
getTierEffects,
|
||||
getTwinkle,
|
||||
} from '../src/remotion/gacha-timeline.js';
|
||||
|
||||
describe('gacha timeline', () => {
|
||||
@@ -38,13 +40,13 @@ describe('gacha timeline', () => {
|
||||
expect(new Set(glows).size).toBe(3);
|
||||
});
|
||||
|
||||
test('meteor falls from off-screen upper right toward the lower left', () => {
|
||||
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).toBeGreaterThan(640);
|
||||
expect(start.x).toBeLessThan(0);
|
||||
expect(start.y).toBeLessThan(0);
|
||||
expect(end.x).toBeLessThan(start.x);
|
||||
expect(end.y).toBeGreaterThan(start.y);
|
||||
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);
|
||||
});
|
||||
@@ -54,6 +56,18 @@ describe('gacha timeline', () => {
|
||||
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));
|
||||
});
|
||||
|
||||
Reference in new issue
Block a user