release/0.1.0 #18
5 changed files with 49 additions and 5 deletions
|
|
@ -109,8 +109,9 @@ describe("GameEngine", () => {
|
|||
const ack = emits.find((e) => e.event === "round:voteAck")!.payload as {
|
||||
count: number
|
||||
total: number
|
||||
voted: string[]
|
||||
}
|
||||
expect(ack).toEqual({ count: 1, total: 2 })
|
||||
expect(ack).toEqual({ count: 1, total: 2, voted: [p1] })
|
||||
|
||||
engine.handleVote(p2, { choiceIndex: 0 })
|
||||
await run
|
||||
|
|
|
|||
|
|
@ -73,6 +73,7 @@ export class GameEngine implements RoomGameController {
|
|||
this.io.to(this.room.code).emit("round:voteAck", {
|
||||
count: this.runtime.votes.size,
|
||||
total,
|
||||
voted: [...this.runtime.votes.keys()],
|
||||
})
|
||||
// Fin anticipée : tous les éligibles ont voté.
|
||||
if (total > 0 && this.runtime.votes.size >= total) {
|
||||
|
|
|
|||
|
|
@ -1,14 +1,23 @@
|
|||
import { AnimatePresence, motion } from "framer-motion"
|
||||
import { Crown } from "lucide-react"
|
||||
import { Check, Crown } from "lucide-react"
|
||||
import type { RoomSnapshot } from "@nerdware/shared"
|
||||
|
||||
interface PlayerCardsProps {
|
||||
snapshot: RoomSnapshot
|
||||
playerId: string | null
|
||||
/** Affiche l'état de vote (manche en cours uniquement). */
|
||||
showVoteStatus?: boolean
|
||||
/** playerIds ayant déjà validé leur réponse. */
|
||||
votedIds?: string[]
|
||||
}
|
||||
|
||||
/** HUD persistant : une card par joueur avec son score, couronne au leader. */
|
||||
export function PlayerCards({ snapshot, playerId }: PlayerCardsProps) {
|
||||
export function PlayerCards({
|
||||
snapshot,
|
||||
playerId,
|
||||
showVoteStatus = false,
|
||||
votedIds = [],
|
||||
}: PlayerCardsProps) {
|
||||
const scoreOf = (id: string) =>
|
||||
snapshot.scores.find((s) => s.playerId === id)?.score ?? 0
|
||||
|
||||
|
|
@ -16,6 +25,7 @@ export function PlayerCards({ snapshot, playerId }: PlayerCardsProps) {
|
|||
const ranked = [...snapshot.players].sort(
|
||||
(a, b) => scoreOf(b.id) - scoreOf(a.id)
|
||||
)
|
||||
const voted = new Set(votedIds)
|
||||
|
||||
return (
|
||||
<div className="flex flex-wrap justify-center gap-2">
|
||||
|
|
@ -24,6 +34,7 @@ export function PlayerCards({ snapshot, playerId }: PlayerCardsProps) {
|
|||
const score = scoreOf(p.id)
|
||||
const isLeader = max > 0 && score === max
|
||||
const isMe = p.id === playerId
|
||||
const hasVoted = voted.has(p.id)
|
||||
return (
|
||||
<motion.div
|
||||
key={p.id}
|
||||
|
|
@ -51,6 +62,7 @@ export function PlayerCards({ snapshot, playerId }: PlayerCardsProps) {
|
|||
{p.name}
|
||||
{isMe && <span className="text-muted-foreground"> (toi)</span>}
|
||||
</span>
|
||||
|
||||
<motion.span
|
||||
key={score}
|
||||
initial={{ scale: 1.4 }}
|
||||
|
|
@ -60,6 +72,27 @@ export function PlayerCards({ snapshot, playerId }: PlayerCardsProps) {
|
|||
>
|
||||
{score}
|
||||
</motion.span>
|
||||
|
||||
{showVoteStatus &&
|
||||
(hasVoted ? (
|
||||
<motion.span
|
||||
initial={{ scale: 0 }}
|
||||
animate={{ scale: 1 }}
|
||||
transition={{ type: "spring", stiffness: 600, damping: 18 }}
|
||||
className="flex items-center gap-0.5 text-[10px] font-medium text-green-500"
|
||||
>
|
||||
<Check className="size-3" /> Prêt
|
||||
</motion.span>
|
||||
) : (
|
||||
<span className="flex items-center gap-1 text-[10px]">
|
||||
<motion.span
|
||||
animate={{ opacity: [0.3, 1, 0.3] }}
|
||||
transition={{ duration: 1.1, repeat: Infinity }}
|
||||
className="bg-muted-foreground size-1.5 rounded-full"
|
||||
/>
|
||||
<span className="text-muted-foreground">en attente</span>
|
||||
</span>
|
||||
))}
|
||||
</motion.div>
|
||||
)
|
||||
})}
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ export function RoomPage({ code }: { code: string }) {
|
|||
const playerId = useRoomStore((s) => s.playerId)
|
||||
const connected = useRoomStore((s) => s.connected)
|
||||
const boomKey = useRoomStore((s) => s.boomKey)
|
||||
const voteProgress = useRoomStore((s) => s.voteProgress)
|
||||
|
||||
// Pas de snapshot pour ce code (accès direct / refresh) : retour à l'accueil.
|
||||
if (!snapshot || snapshot.code !== code) {
|
||||
|
|
@ -45,7 +46,14 @@ export function RoomPage({ code }: { code: string }) {
|
|||
/>
|
||||
</header>
|
||||
|
||||
{inGame && <PlayerCards snapshot={snapshot} playerId={playerId} />}
|
||||
{inGame && (
|
||||
<PlayerCards
|
||||
snapshot={snapshot}
|
||||
playerId={playerId}
|
||||
showVoteStatus={snapshot.status === "in_round"}
|
||||
votedIds={voteProgress?.voted}
|
||||
/>
|
||||
)}
|
||||
|
||||
{snapshot.status === "lobby" && <LobbyView snapshot={snapshot} />}
|
||||
{inGame && <QuizView snapshot={snapshot} />}
|
||||
|
|
|
|||
|
|
@ -73,10 +73,11 @@ export interface VotePayload {
|
|||
answer: Answer
|
||||
}
|
||||
|
||||
/** Progression anonyme des votes. */
|
||||
/** Progression des votes. `voted` = playerIds ayant validé (jamais leur réponse). */
|
||||
export interface VoteAckPayload {
|
||||
count: number
|
||||
total: number
|
||||
voted: string[]
|
||||
}
|
||||
|
||||
export interface RevealPayload {
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue