- +

Draft Completed

@@ -1012,12 +1019,16 @@
+
@@ -2893,6 +2905,7 @@ + -../../../src/SetInfos diff --git a/client/src/components/SealedPresentation.vue b/client/src/components/SealedPresentation.vue new file mode 100644 index 000000000..3a6c39d04 --- /dev/null +++ b/client/src/components/SealedPresentation.vue @@ -0,0 +1,75 @@ + + + + + diff --git a/client/src/css/app.css b/client/src/css/app.css index c4770f4fc..fa7af0f9d 100644 --- a/client/src/css/app.css +++ b/client/src/css/app.css @@ -1018,6 +1018,7 @@ ul.player-list { /* Booster passing animation */ +.booster-fade-in-enter-active, .slide-fade-right-enter-active, .slide-fade-right-leave-active, .slide-fade-left-enter-active, @@ -1037,6 +1038,11 @@ ul.player-list { opacity: 0; } +.booster-fade-in-enter-from { + opacity: 0; +} + +.booster-open-leave-active, .booster-cards-enter-active, .booster-cards-leave-active { pointer-events: none; /* Avoid picking cards during transition */ @@ -1047,16 +1053,19 @@ ul.player-list { opacity: 0; } +.booster-open-leave-active.selected, .booster-cards-leave-active.selected { transition: all 0.5s; width: calc(var(--booster-card-scale) * 200px); overflow: visible; } +.booster-open-leave-active.selected .card-image, .booster-cards-leave-active.selected .card-image { width: calc(var(--booster-card-scale) * 200px); } +.booster-open-leave-to.selected, .booster-cards-leave-to.selected { transform: translateY(250px) translateX(calc(var(--booster-card-scale) * -100px)); /* translateX compensates for width change */ z-index: 2; @@ -1065,10 +1074,12 @@ ul.player-list { margin: 0; } +.booster-open-leave-active.burned, .booster-cards-leave-active.burned { animation: card-burn 0.5s; } +.booster-open-leave-to.burned, .booster-cards-leave-to.burned { margin: 0; } diff --git a/client/src/css/booster-open.css b/client/src/css/booster-open.css new file mode 100644 index 000000000..c0a553e1f --- /dev/null +++ b/client/src/css/booster-open.css @@ -0,0 +1,188 @@ +.booster-open-enter-active { + --intro-duration: 0.5s; + --translation-duration: 0.5s; + --flip-duration: 0.4s; + --enter-delay-interval: 0.04s; + + --animation-index: 20; + --enter-delay: calc((var(--animation-index) - 1) * var(--enter-delay-interval)); + + animation: + booster-card-enter-together var(--intro-duration) ease 0s 1, + booster-card-enter var(--translation-duration) cubic-bezier(0.34, 0, 0.3, 1) + calc(var(--intro-duration) + var(--enter-delay)) 1; + animation-fill-mode: forwards, forwards; + transform-origin: center 140%; +} + +.booster-open-enter-active:nth-child(1) { + --animation-index: 1; +} +.booster-open-enter-active:nth-child(2) { + --animation-index: 2; +} +.booster-open-enter-active:nth-child(3) { + --animation-index: 3; +} +.booster-open-enter-active:nth-child(4) { + --animation-index: 4; +} +.booster-open-enter-active:nth-child(5) { + --animation-index: 5; +} +.booster-open-enter-active:nth-child(6) { + --animation-index: 6; +} +.booster-open-enter-active:nth-child(7) { + --animation-index: 7; +} +.booster-open-enter-active:nth-child(8) { + --animation-index: 8; +} +.booster-open-enter-active:nth-child(9) { + --animation-index: 9; +} +.booster-open-enter-active:nth-child(10) { + --animation-index: 10; +} +.booster-open-enter-active:nth-child(11) { + --animation-index: 11; +} +.booster-open-enter-active:nth-child(12) { + --animation-index: 12; +} +.booster-open-enter-active:nth-child(13) { + --animation-index: 13; +} +.booster-open-enter-active:nth-child(14) { + --animation-index: 14; +} +.booster-open-enter-active:nth-child(15) { + --animation-index: 15; +} +.booster-open-enter-active:nth-child(16) { + --animation-index: 16; +} +.booster-open-enter-active:nth-child(17) { + --animation-index: 17; +} +.booster-open-enter-active:nth-child(18) { + --animation-index: 18; +} +.booster-open-enter-active:nth-child(19) { + --animation-index: 19; +} +.booster-open-enter-active:nth-child(20) { + --animation-index: 20; +} + +@keyframes booster-card-enter-together { + 0% { + transform: translate(var(--initial-translation-x), var(--initial-translation-y)) rotate(0) scale(0); + } + 30% { + transform: translate(var(--initial-translation-x), var(--initial-translation-y)) rotate(0) scale(1.05); + } + 90% { + transform: translate(var(--initial-translation-x), calc(var(--initial-translation-y))) + rotate(var(--initial-rotation)) scale(1); + } + 100% { + transform: translate(var(--initial-translation-x), var(--initial-translation-y)) rotate(var(--initial-rotation)); + } +} + +@keyframes booster-card-enter { + 0% { + transform: translate(var(--initial-translation-x), 0) rotate(var(--initial-rotation)); + } + 35% { + transform: translate(0, 0); + } + 75% { + transform: translate(0, 0); + } + 100% { + transform: translate(0, 0); + } +} + +/* This won't work on Firefox right now since it doesn't have :has support yet, but that's ok. It's purely for aesthetic. */ +div:has(> .booster-open-enter-active):not(:has(> .booster-open-leave-active))::before { + content: ""; + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + width: 440px; + height: 440px; + --beam-color: #ffffff80; + background-image: repeating-conic-gradient(#ffffffa0 0, #000 18deg, #ffffffa0 36deg); + background-size: cover; + background-position: center; + -webkit-mask: radial-gradient(circle at center, #fff 0%, transparent 70%); + mask: radial-gradient(circle at center, #fff 0%, transparent 70%); + + opacity: 0; + transform-origin: center; + animation: booster-cards-background-animation 0.5s linear forwards; +} + +@keyframes booster-cards-background-animation { + 0% { + transform: translate(-50%, -50%) rotate(0deg) scale(0); + opacity: 0; + } + 20% { + transform: translate(-50%, -50%) rotate(calc(0.2 * 90deg)) scale(1); + opacity: 1; + } + 80% { + opacity: 1; + } + 100% { + transform: translate(-50%, -50%) rotate(90deg); + opacity: 0; + } +} + +.booster-open-enter-active .flip-container { + animation: + booster-card-enter-translate calc(var(--translation-duration)) ease + calc(var(--intro-duration) + var(--enter-delay)) 1, + booster-card-enter-flip var(--flip-duration) ease-in + calc(var(--intro-duration) + var(--translation-duration) + var(--enter-delay)) 1; + animation-fill-mode: none, forwards; + + /* Necessary for the transform origin to be correct */ + width: 100%; + height: 100%; + transform: scale(1) translate(0, 0) rotateY(-180deg); +} + +@keyframes booster-card-enter-translate { + 0% { + transform: scale(1) translate(0, var(--initial-translation-y)) rotateY(-180deg); + } + 100% { + transform: scale(1) translate(0, 0) rotateY(-180deg); + } +} + +@keyframes booster-card-enter-flip { + 0% { + transform: scale(1) translate(0, 0) rotateY(-180deg); + } + 25% { + transform: scale(1.15) translate(0, 10px) rotateY(-180deg); + } + 50% { + transform: scale(1.15) rotateY(-90deg); + } + 75% { + transform: scale(1.15) translate(0, -10px) rotateY(0deg); + } + 100% { + transform: scale(1) rotateY(0); + } +} diff --git a/client/src/helper.ts b/client/src/helper.ts index a0855f6e6..41da223cc 100644 --- a/client/src/helper.ts +++ b/client/src/helper.ts @@ -192,3 +192,19 @@ export function fitFontSize( } target.classList.remove("fitting"); } + +// Should be called on enter for the booster-open transition +export function onEnterBoosterCards(e: Element) { + const el = e as HTMLElement; + const p = el.parentElement; + if (p) { + const target = [p.offsetLeft + p.clientWidth / 2, p.offsetTop + p.clientHeight / 2]; + const center = [el.offsetLeft + el.clientWidth / 2, el.offsetTop + el.clientHeight / 2]; + const offset = [target[0] - center[0], target[1] - center[1]]; + const index = Array.from(p.children).indexOf(el); + const rotation = index - p.children.length / 2; + el.style.setProperty("--initial-translation-x", `${offset[0]}px`); + el.style.setProperty("--initial-translation-y", `${offset[1]}px`); + el.style.setProperty("--initial-rotation", `${rotation}deg`); + } +} diff --git a/src/Session.ts b/src/Session.ts index 34b4566ce..de8736ba5 100644 --- a/src/Session.ts +++ b/src/Session.ts @@ -2981,10 +2981,10 @@ export class Session implements IIndexable { playersBoosters.push(boosters[currIdx]); currIdx += this.users.size; } - Connections[userID].socket.emit("setCardSelection", playersBoosters); Connections[userID].pickedCards.main = playersBoosters.flat(); Connections[userID].pickedCards.side = []; - log.users[userID].cards = playersBoosters.flat().map((c) => c.id); + log.users[userID].cards = Connections[userID].pickedCards.main.flat().map((c) => c.id); + Connections[userID].socket.emit("sealedBoosters", playersBoosters); ++idx; } @@ -3175,12 +3175,12 @@ export class Session implements IIndexable { const BoosterImage = { jmp: "/img/2JumpstartBoosters.webp", j22: "/img/2Jumpstart2022Boosters.webp" }[set]; for (const user of this.users) { const boosters = [getRandom(JMPBoosters), getRandom(JMPBoosters)]; - const cards = boosters.map((b) => b.cards.map((cid: CardID) => getUnique(cid))); + const cards = boosters.map((b) => b.cards.map((cid: CardID) => getUnique(cid))).flat(); - log.users[user].cards = cards.flat().map((c: Card) => c.id); + log.users[user].cards = cards.map((c: Card) => c.id); for (const cid of log.users[user].cards) log.carddata[cid] = getCard(cid); - Connections[user].socket.emit("setCardSelection", cards); + Connections[user].socket.emit("setCardPool", cards); Connections[user].socket.emit("message", { icon: "success", imageUrl: BoosterImage, diff --git a/src/SocketType.ts b/src/SocketType.ts index 967cda888..d587d28a9 100644 --- a/src/SocketType.ts +++ b/src/SocketType.ts @@ -96,7 +96,8 @@ export interface ServerToClientEvents { resumeOnReconnection: (msg: Message) => void; - setCardSelection: (boosters: UniqueCard[][]) => void; + sealedBoosters: (boosters: UniqueCard[][]) => void; + setCardPool: (cards: UniqueCard[]) => void; addCards: (message: string, cards: UniqueCard[]) => void; updateCardState: (updates: { cardID: UniqueCardID; state: UniqueCardState }[]) => void; diff --git a/test/test.ts b/test/test.ts index 701a9f527..39e004755 100644 --- a/test/test.ts +++ b/test/test.ts @@ -2168,7 +2168,7 @@ describe("Sealed", function () { const ownerIdx = clients.findIndex((c) => getUID(c) === Sessions[sessionID].owner); let receivedPools = 0; for (const client of clients) - client.once("setCardSelection", (boosters) => { + client.once("sealedBoosters", (boosters) => { expect(boosters.length).to.equal(boosterCount); ++receivedPools; if (receivedPools === clients.length) done(); @@ -2191,7 +2191,7 @@ describe("Sealed", function () { const ownerIdx = clients.findIndex((c) => getUID(c) === Sessions[sessionID].owner); let receivedPools = 0; for (const client of clients) - client.once("setCardSelection", (boosters) => { + client.once("sealedBoosters", (boosters) => { expect(boosters.length).to.equal(boosterCount); for (let idx = 0; idx < boosters.length; ++idx) expect(boosters[idx].every((c) => c.set === CustomBoosters[idx])); @@ -2257,13 +2257,12 @@ describe("Jumpstart", function () { done(); }); - it(`Owner launches a Jumpstart game, clients should receive their card selection (2*20 cards).`, function (done) { + it(`Owner launches a Jumpstart game, clients should receive their card selection (40 cards).`, function (done) { const ownerIdx = clients.findIndex((c) => getUID(c) === Sessions[sessionID].owner); let receivedPools = 0; for (const client of clients) { - client.once("setCardSelection", function (boosters) { - expect(boosters.length).to.equal(2); - for (const b of boosters) expect(b.length).to.equal(20); + client.once("setCardPool", function (cards) { + expect(cards.length).to.equal(40); ++receivedPools; if (receivedPools === clients.length) done(); }); @@ -2315,13 +2314,12 @@ describe("Jumpstart 2022", function () { done(); }); - it(`Owner launches a Jumpstart 2022 game, clients should receive their card selection (2*20 cards).`, function (done) { + it(`Owner launches a Jumpstart 2022 game, clients should receive their card selection (40 cards).`, function (done) { const ownerIdx = clients.findIndex((c) => getUID(c) === Sessions[sessionID].owner); let receivedPools = 0; for (const client of clients) { - client.once("setCardSelection", function (boosters) { - expect(boosters.length).to.equal(2); - for (const b of boosters) expect(b.length).to.equal(20); + client.once("setCardPool", function (cards) { + expect(cards.length).to.equal(40); ++receivedPools; if (receivedPools === clients.length) done(); });