mirror of
https://github.com/tiennm99/tiennm99bot.git
synced 2026-10-16 00:15:37 +00:00
feat(gacha): rebuild the beta wish as a meteor shower reveal
The beta wish now renders an 8-second night sky over a snow-rimmed mountain ridge, where coloured meteors glide down the diagonal. A hero meteor in the rarity colour slows and burns out at the centre, where the label appears under rank SSS. The toon cloud, comet, and perspective camera are removed. The /api/gachabeta contract is unchanged.
This commit is contained in:
1 parent
adea684a2f
commit
b8d5544fd6
5 files changed
+370
-761
No files matched your search
@@ -3,101 +3,67 @@ import {
|
||||
gachaBetaTimeline,
|
||||
gachaBetaTotalSeconds,
|
||||
getBetaRank,
|
||||
getBetaShake,
|
||||
getCamera,
|
||||
getCometGlow,
|
||||
getCometPosition,
|
||||
heroCloud,
|
||||
pierceTime,
|
||||
project,
|
||||
getBurnGlint,
|
||||
getHeroMeteor,
|
||||
getSkyMeteorHead,
|
||||
meteorAngleDegrees,
|
||||
revealAnchor,
|
||||
skyMeteors,
|
||||
} from '../src/remotion/gacha-beta-scene.js';
|
||||
|
||||
describe('gacha beta scene', () => {
|
||||
test('phases run in order inside the clip', () => {
|
||||
const width = 640;
|
||||
const height = 360;
|
||||
|
||||
describe('gacha beta meteor scene', () => {
|
||||
test('beats run in order and the result holds before the clip ends', () => {
|
||||
const tl = gachaBetaTimeline;
|
||||
expect(tl.cometAppear).toBeLessThan(tl.pierceStart);
|
||||
expect(tl.pierceStart).toBeLessThan(pierceTime);
|
||||
expect(pierceTime).toBeLessThan(tl.pierceEnd);
|
||||
expect(tl.pierceEnd).toBeLessThan(tl.flightEnd);
|
||||
expect(tl.flightEnd).toBeLessThan(tl.flashPeak);
|
||||
expect(tl.cardIn).toBeLessThan(tl.flashEnd);
|
||||
expect(tl.rankIn + 1).toBeLessThan(gachaBetaTotalSeconds);
|
||||
expect(tl.fadeInEnd).toBeLessThan(tl.heroStart);
|
||||
expect(tl.heroStart).toBeLessThan(tl.heroBurn);
|
||||
expect(tl.heroBurn).toBeLessThan(tl.labelIn);
|
||||
expect(tl.labelIn).toBeLessThan(tl.rankIn);
|
||||
expect(tl.rankIn + 2).toBeLessThan(gachaBetaTotalSeconds);
|
||||
});
|
||||
|
||||
test('comet waits behind the hero cloud, then crosses its plane at the pierce', () => {
|
||||
expect(getCometPosition(gachaBetaTimeline.cometAppear).z).toBeGreaterThan(heroCloud.z);
|
||||
expect(getCometPosition(pierceTime).z).toBeCloseTo(heroCloud.z);
|
||||
expect(getCometPosition(gachaBetaTimeline.pierceEnd).z).toBeLessThan(heroCloud.z);
|
||||
});
|
||||
|
||||
test('comet is hidden behind the cloud: projected inside its silhouette', () => {
|
||||
const seconds = gachaBetaTimeline.pierceStart;
|
||||
const camera = getCamera(seconds);
|
||||
const comet = project(getCometPosition(seconds), camera, 640, 360);
|
||||
const cloud = project(heroCloud, camera, 640, 360);
|
||||
expect(comet && cloud).toBeTruthy();
|
||||
if (comet && cloud) {
|
||||
expect(Math.hypot(comet.x - cloud.x, comet.y - cloud.y)).toBeLessThan(heroCloud.radius * cloud.scale);
|
||||
expect(comet.depth).toBeGreaterThan(cloud.depth);
|
||||
}
|
||||
});
|
||||
|
||||
test('approaching comet is visible beside the cloud before passing behind it', () => {
|
||||
const seconds = 1.4;
|
||||
const camera = getCamera(seconds);
|
||||
const comet = project(getCometPosition(seconds), camera, 640, 360);
|
||||
const cloud = project(heroCloud, camera, 640, 360);
|
||||
expect(comet && cloud).toBeTruthy();
|
||||
if (comet && cloud) {
|
||||
expect(comet.x).toBeGreaterThan(0);
|
||||
expect(comet.x).toBeLessThan(640);
|
||||
expect(comet.y).toBeGreaterThan(0);
|
||||
expect(comet.y).toBeLessThan(360);
|
||||
expect(Math.hypot(comet.x - cloud.x, comet.y - cloud.y)).toBeGreaterThan(heroCloud.radius * cloud.scale);
|
||||
expect(comet.depth).toBeGreaterThan(cloud.depth);
|
||||
}
|
||||
});
|
||||
|
||||
test('camera dollies toward the cloud, then chases the comet and keeps it in frame', () => {
|
||||
expect(getCamera(1).z).toBeGreaterThan(getCamera(0).z);
|
||||
for (const seconds of [3, 3.5, 4, 4.5]) {
|
||||
const camera = getCamera(seconds);
|
||||
const comet = project(getCometPosition(seconds), camera, 640, 360);
|
||||
expect(comet).not.toBeNull();
|
||||
if (comet) {
|
||||
expect(comet.x).toBeGreaterThan(640 * 0.2);
|
||||
expect(comet.x).toBeLessThan(640 * 0.8);
|
||||
expect(comet.y).toBeGreaterThan(360 * 0.2);
|
||||
expect(comet.y).toBeLessThan(360 * 0.8);
|
||||
test('background meteors appear on their cue and glide down to the right', () => {
|
||||
for (const meteor of skyMeteors) {
|
||||
expect(getSkyMeteorHead(meteor, meteor.start - 0.01, width, height)).toBeNull();
|
||||
const a = getSkyMeteorHead(meteor, meteor.start + 0.5, width, height);
|
||||
const b = getSkyMeteorHead(meteor, meteor.start + 1.5, width, height);
|
||||
expect(a && b).toBeTruthy();
|
||||
if (a && b) {
|
||||
expect(b.x).toBeGreaterThan(a.x);
|
||||
expect(b.y).toBeGreaterThan(a.y);
|
||||
expect((Math.atan2(b.y - a.y, b.x - a.x) * 180) / Math.PI).toBeCloseTo(meteorAngleDegrees);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
test('camera never moves backward', () => {
|
||||
let previous = -Infinity;
|
||||
for (let seconds = 0; seconds <= gachaBetaTimeline.flightEnd; seconds += 0.05) {
|
||||
const {z} = getCamera(seconds);
|
||||
expect(z).toBeGreaterThanOrEqual(previous - 1e-6);
|
||||
previous = z;
|
||||
test('the volley stays in the upper sky so the result area is clear at the reveal', () => {
|
||||
for (const meteor of skyMeteors) {
|
||||
const head = getSkyMeteorHead(meteor, gachaBetaTimeline.labelIn, width, height);
|
||||
if (head) {
|
||||
expect(head.y).toBeLessThan(height * 0.62);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
test('projection drops points behind the camera', () => {
|
||||
expect(project({x: 0, y: 0, z: 10}, {x: 0, y: 0, z: 0, roll: 0}, 640, 360)).toBeNull();
|
||||
const centre = project({x: 0, y: 0, z: 500}, {x: 0, y: 0, z: 0, roll: 0}, 640, 360);
|
||||
expect(centre).toMatchObject({x: 320, y: 180});
|
||||
test('hero meteor is hidden before its cue, then slows to burn out on the anchor', () => {
|
||||
const tl = gachaBetaTimeline;
|
||||
expect(getHeroMeteor(tl.heroStart - 0.1, width, height).visible).toBe(0);
|
||||
const early = getHeroMeteor(tl.heroStart + 0.5, width, height);
|
||||
const mid = getHeroMeteor(tl.heroStart + 1, width, height);
|
||||
const late = getHeroMeteor(tl.heroBurn - 0.5, width, height);
|
||||
const late2 = getHeroMeteor(tl.heroBurn, width, height);
|
||||
expect(Math.hypot(mid.x - early.x, mid.y - early.y)).toBeGreaterThan(Math.hypot(late2.x - late.x, late2.y - late.y));
|
||||
expect(late2.x).toBeCloseTo(revealAnchor.x * width);
|
||||
expect(late2.y).toBeCloseTo(revealAnchor.y * height);
|
||||
expect(getHeroMeteor(tl.heroBurn + 0.2, width, height).visible).toBe(0);
|
||||
});
|
||||
|
||||
test('comet glow builds to full by the burst', () => {
|
||||
expect(getCometGlow(0)).toBe(0);
|
||||
expect(getCometGlow(3)).toBeGreaterThan(getCometGlow(2));
|
||||
expect(getCometGlow(gachaBetaTimeline.flightEnd)).toBeCloseTo(1);
|
||||
});
|
||||
|
||||
test('shake fires at the pierce and the burst only', () => {
|
||||
expect(getBetaShake(1, 360)).toEqual({x: 0, y: 0});
|
||||
expect(Math.hypot(...Object.values(getBetaShake(pierceTime + 0.05, 360)))).toBeGreaterThan(0);
|
||||
expect(Math.hypot(...Object.values(getBetaShake(gachaBetaTimeline.flightEnd + 0.05, 360)))).toBeGreaterThan(0);
|
||||
test('burn glint fires only around the burn-out', () => {
|
||||
expect(getBurnGlint(gachaBetaTimeline.heroBurn - 1)).toBe(0);
|
||||
expect(getBurnGlint(gachaBetaTimeline.heroBurn)).toBe(1);
|
||||
expect(getBurnGlint(gachaBetaTimeline.heroBurn + 2)).toBe(0);
|
||||
});
|
||||
|
||||
test('beta food reveal displays SSS', () => {
|
||||
|
||||
Reference in new issue
Block a user