Skip to content

Commit 3968c9b

Browse files
committed
Еноткики добавлены в Adepts-game 3
1 parent a57694a commit 3968c9b

2 files changed

Lines changed: 149 additions & 11 deletions

File tree

artifacts/game-client/src/apps/adepts-game-3/components/QuestionModal.tsx

Lines changed: 117 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,99 @@ function isVideo(url: string) {
3333
return /\.(mp4|webm|ogg)$/i.test(url);
3434
}
3535

36+
// ── Spiral + SplashOverlay ──────────────────────────────────────────────────
37+
const MAGIC_COLORS = ["#FFD700","#FFFFFF","#FF88FF","#44FFFF","#FFAA44","#FF44AA","#BBFFAA","#FF6644"];
38+
const SPLASH_DURATION = 3500;
39+
40+
const SPIRAL = (() => {
41+
const rotations = 2, steps = 48, rxMax = 960, ryMax = 560;
42+
const xs: number[] = [], ys: number[] = [], scales: number[] = [], times: number[] = [];
43+
for (let i = 0; i <= steps; i++) {
44+
const t = i / steps, theta = t * rotations * 2 * Math.PI;
45+
const rx = rxMax * (1 - t), ry = ryMax * (1 - t);
46+
xs.push(Math.round(rx * Math.sin(theta)));
47+
ys.push(Math.round(-ry * Math.cos(theta)));
48+
scales.push(Math.round((0.05 + 0.95 * t) * 100) / 100);
49+
times.push(Math.round(t * 10000) / 10000);
50+
}
51+
return { xs, ys, scales, times };
52+
})();
53+
54+
type SparkParticle = { x: number; y: number; vx: number; vy: number; alpha: number; size: number; color: string; life: number; decay: number; };
55+
56+
function interpolateSpiral(t: number) {
57+
const times = SPIRAL.times;
58+
let i = times.length - 2;
59+
for (let j = 0; j < times.length - 1; j++) { if (t <= times[j + 1]) { i = j; break; } }
60+
const seg = times[i + 1] === times[i] ? 0 : (t - times[i]) / (times[i + 1] - times[i]);
61+
return { x: SPIRAL.xs[i] + (SPIRAL.xs[i + 1] - SPIRAL.xs[i]) * seg, y: SPIRAL.ys[i] + (SPIRAL.ys[i + 1] - SPIRAL.ys[i]) * seg };
62+
}
63+
64+
function SplashOverlay({ url, onDismiss }: { url: string; onDismiss: () => void }) {
65+
const canvasRef = useRef<HTMLCanvasElement>(null);
66+
const particlesRef = useRef<SparkParticle[]>([]);
67+
const rafRef = useRef<number | undefined>(undefined);
68+
const startRef = useRef<number>(0);
69+
70+
useEffect(() => {
71+
const canvas = canvasRef.current;
72+
if (!canvas) return;
73+
const ctx = canvas.getContext("2d");
74+
if (!ctx) return;
75+
const resize = () => { canvas.width = window.innerWidth; canvas.height = window.innerHeight; };
76+
resize();
77+
window.addEventListener("resize", resize);
78+
startRef.current = performance.now();
79+
particlesRef.current = [];
80+
81+
const loop = () => {
82+
rafRef.current = requestAnimationFrame(loop);
83+
ctx.clearRect(0, 0, canvas.width, canvas.height);
84+
const elapsed = performance.now() - startRef.current;
85+
const t = Math.min(elapsed / SPLASH_DURATION, 1);
86+
const pos = interpolateSpiral(t);
87+
const cx = canvas.width / 2 + pos.x, cy = canvas.height / 2 + pos.y;
88+
if (t < 0.98) {
89+
const sparkCount = Math.round(6 + t * 20);
90+
const currentScale = 0.05 + 0.95 * t;
91+
const maxRadius = Math.min(canvas.width * 0.39, canvas.height * 0.39);
92+
const raccoonRadius = maxRadius * currentScale;
93+
for (let k = 0; k < sparkCount; k++) {
94+
const spawnAngle = Math.random() * Math.PI * 2, speed = 0.8 + Math.random() * 3.5;
95+
const spawnX = cx + Math.cos(spawnAngle) * raccoonRadius, spawnY = cy + Math.sin(spawnAngle) * raccoonRadius;
96+
particlesRef.current.push({ x: spawnX, y: spawnY, vx: Math.cos(spawnAngle) * speed + (Math.random() - 0.5) * 1.5, vy: Math.sin(spawnAngle) * speed - 0.8 + (Math.random() - 0.5) * 1.5, alpha: 1, size: 2 + Math.random() * 5, color: MAGIC_COLORS[Math.floor(Math.random() * MAGIC_COLORS.length)], life: 0, decay: 0.016 + Math.random() * 0.024 });
97+
}
98+
}
99+
const alive: SparkParticle[] = [];
100+
for (const p of particlesRef.current) {
101+
p.life += p.decay; p.x += p.vx; p.y += p.vy; p.vy += 0.1; p.vx *= 0.97; p.size *= 0.97; p.alpha = Math.max(0, 1 - p.life);
102+
if (p.life < 1 && p.size > 0.3) {
103+
alive.push(p);
104+
ctx.save(); ctx.globalAlpha = p.alpha * 0.85; ctx.shadowBlur = 14; ctx.shadowColor = p.color; ctx.fillStyle = p.color;
105+
ctx.beginPath(); ctx.arc(p.x, p.y, p.size, 0, Math.PI * 2); ctx.fill();
106+
ctx.globalAlpha = p.alpha * 0.55; ctx.shadowBlur = 0; ctx.fillStyle = "#FFFFFF";
107+
ctx.beginPath(); ctx.arc(p.x, p.y, p.size * 0.38, 0, Math.PI * 2); ctx.fill(); ctx.restore();
108+
}
109+
}
110+
particlesRef.current = alive;
111+
};
112+
loop();
113+
return () => { if (rafRef.current) cancelAnimationFrame(rafRef.current); window.removeEventListener("resize", resize); };
114+
}, []);
115+
116+
return (
117+
<motion.div className="fixed inset-0 z-[200] flex items-center justify-center cursor-pointer select-none" onClick={onDismiss} initial={{ opacity: 0 }} animate={{ opacity: 1 }} exit={{ opacity: 0, transition: { duration: 0.25 } }}>
118+
<canvas ref={canvasRef} className="absolute inset-0 pointer-events-none" />
119+
<motion.img src={resolveUrl(url)} alt="" draggable={false}
120+
initial={{ x: SPIRAL.xs[0], y: SPIRAL.ys[0], scale: 0.05, rotate: 0 }}
121+
animate={{ x: SPIRAL.xs, y: SPIRAL.ys, scale: SPIRAL.scales, rotate: 0, transition: { duration: 3.5, ease: "linear", times: SPIRAL.times } }}
122+
exit={{ scale: 0, opacity: 0, transition: { duration: 0.28, ease: "easeIn" } }}
123+
style={{ maxWidth: "78vw", maxHeight: "78vh", objectFit: "contain", pointerEvents: "none", position: "relative" }}
124+
/>
125+
</motion.div>
126+
);
127+
}
128+
36129
const FW_COLORS = [
37130
"#FFD700","#FF4444","#44DDFF","#FF44FF","#44FF88",
38131
"#FF8844","#FFFFFF","#FFAA00","#AA44FF","#44FFFF",
@@ -170,6 +263,7 @@ export function QuestionModal({
170263
const [awarded, setAwarded] = useState<number | null>(null);
171264
const [countdown, setCountdown] = useState(TIMER_SECONDS);
172265
const [showFireworks, setShowFireworks] = useState(false);
266+
const [splashDismissed, setSplashDismissed] = useState(false);
173267
const intervalRef = useRef<ReturnType<typeof setInterval> | null>(null);
174268
const isCelebration = (themeName === "Зацени Look" && points === 500) || (themeName === "Боссы" && points === 200);
175269

@@ -203,6 +297,7 @@ export function QuestionModal({
203297
setStage("question");
204298
setIsEditing(false);
205299
setShowFireworks(false);
300+
setSplashDismissed(false);
206301
startTimer();
207302
} else {
208303
stopTimer();
@@ -254,6 +349,11 @@ export function QuestionModal({
254349
return (
255350
<>
256351
<Fireworks active={showFireworks} />
352+
<AnimatePresence>
353+
{isOpen && question.splashUrl && !splashDismissed && (
354+
<SplashOverlay url={question.splashUrl} onDismiss={() => setSplashDismissed(true)} />
355+
)}
356+
</AnimatePresence>
257357
<AnimatePresence>
258358
{isOpen && (
259359
<>
@@ -276,13 +376,23 @@ export function QuestionModal({
276376
{/* Header */}
277377
<div className="flex-shrink-0 flex items-center justify-between px-5 lg:px-8 py-3 lg:py-4 border-b border-border/60 bg-muted/20">
278378
<div className="flex items-center gap-4 lg:gap-6">
279-
<div>
280-
<div className="text-xs font-bold text-accent uppercase tracking-[0.2em] mb-0.5">
281-
{themeName}
282-
</div>
283-
<div className="font-display text-3xl lg:text-5xl text-primary glow-text leading-none">
284-
{points}
379+
<div className="flex items-center gap-2 lg:gap-3">
380+
<div>
381+
<div className="text-xs font-bold text-accent uppercase tracking-[0.2em] mb-0.5">
382+
{themeName}
383+
</div>
384+
<div className="font-display text-3xl lg:text-5xl text-primary glow-text leading-none">
385+
{points}
386+
</div>
285387
</div>
388+
{question.splashUrl && (
389+
<img
390+
src={resolveUrl(question.splashUrl)}
391+
alt=""
392+
className="object-contain select-none pointer-events-none"
393+
style={{ width: "46px", height: "46px", filter: "drop-shadow(0 0 5px hsla(45,100%,60%,0.55))" }}
394+
/>
395+
)}
286396
</div>
287397
<div className="flex items-center gap-2 ml-2">
288398
<div className={`px-3 py-1 rounded-full text-xs font-bold uppercase tracking-wider border transition-all ${stage === "question" ? "bg-primary/20 text-primary border-primary/50" : "bg-muted/30 text-muted-foreground border-border"}`}>
@@ -567,7 +677,7 @@ export function QuestionModal({
567677

568678
{/* Center — timer (only on question stage, not for celebration) */}
569679
<div className="flex justify-center">
570-
{stage === "question" && !isCelebration && (
680+
{stage === "question" && !isCelebration && !question.splashUrl && (
571681
<motion.div
572682
initial={{ opacity: 0, scale: 0.7 }}
573683
animate={{ opacity: 1, scale: 1 }}

artifacts/game-client/src/apps/adepts-game-3/hooks/useGameState.ts

Lines changed: 32 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ export type Question = {
1313
answerText: string;
1414
answerUrl: string;
1515
used: boolean;
16+
splashUrl?: string;
1617
};
1718

1819
export type GameState = {
@@ -63,6 +64,7 @@ const DEFAULT_STATE: GameState = {
6364
questionUrl: "",
6465
answerText: "Экселенс",
6566
answerUrl: "",
67+
splashUrl: "/raccoon.png",
6668
used: false,
6769
},
6870
{
@@ -115,6 +117,7 @@ const DEFAULT_STATE: GameState = {
115117
questionUrl: "https://s.13.cl/sites/default/files/inline-images/2021-01/south-park-wow-cosplayer-1609791777871.jpg",
116118
answerText: "Jarod Nandin — самый знаменитый косплей по WoW.\nКосплей на задрота WoW.",
117119
answerUrl: "https://i.redd.it/rtxt1hffn0r31.jpg",
120+
splashUrl: "/raccoon.png",
118121
used: false,
119122
},
120123
],
@@ -215,6 +218,7 @@ const DEFAULT_STATE: GameState = {
215218
questionUrl: "https://wow.zamimg.com/uploads/screenshots/normal/33806-%D0%BA%D1%80%D0%BE%D0%BC%D0%BA%D0%B0-%D0%BA%D0%B0%D1%82%D0%B0%D0%BA%D0%BB%D0%B8%D0%B7%D0%BC%D0%B0.jpg",
216219
answerText: "Кромка Катаклизма",
217220
answerUrl: gd("1xHf3TlyN7tStHu31AgdeMjn0lxwxIG5m"),
221+
splashUrl: "/raccoon.png",
218222
used: false,
219223
},
220224
{
@@ -284,6 +288,7 @@ const DEFAULT_STATE: GameState = {
284288
questionUrl: "",
285289
answerText: "Спорегар",
286290
answerUrl: "https://static.wikia.nocookie.net/wow/images/4/49/Sporeggar_Concept_Art_Peter_Lee.jpg/revision/latest?cb=20131115184120&path-prefix=ru",
291+
splashUrl: "/raccoon.png",
287292
used: false,
288293
},
289294
{
@@ -343,6 +348,7 @@ const DEFAULT_STATE: GameState = {
343348
questionUrl: "",
344349
answerText: "Подготовка к открытию Врат Ан'киража",
345350
answerUrl: gd("17AFs6awvOAhukzbHGvCyslHPkOm4f_Dt"),
351+
splashUrl: "/raccoon.png",
346352
used: false,
347353
},
348354
],
@@ -351,10 +357,32 @@ const DEFAULT_STATE: GameState = {
351357

352358
const STORAGE_KEY = "adepts-game-3-state";
353359
const PLAYERS_KEY = "adepts-shared-players";
354-
const DATA_VERSION = 11;
360+
const DATA_VERSION = 12;
355361
const DATA_VERSION_KEY = "adepts-game-3-data-version";
356362
const ROOM = "adepts-game-3";
357363

364+
function restoreRaccoonCards(state: GameState): GameState {
365+
const nextQuestions = state.questions.map((theme) => theme.map((question) => ({ ...question })));
366+
const raccoonCards: Array<[number, number]> = [
367+
[0, 2], // Лор Адептов 300
368+
[1, 4], // Всратый косплей 500
369+
[4, 2], // Зацени Look 300
370+
[6, 1], // Фракции 200
371+
[7, 4], // События в WoW 500
372+
];
373+
374+
for (const [themeIdx, questionIdx] of raccoonCards) {
375+
if (nextQuestions[themeIdx]?.[questionIdx]) {
376+
nextQuestions[themeIdx][questionIdx] = {
377+
...nextQuestions[themeIdx][questionIdx],
378+
splashUrl: "/raccoon.png",
379+
};
380+
}
381+
}
382+
383+
return { ...state, questions: nextQuestions };
384+
}
385+
358386
function loadInitialState(): GameState {
359387
try {
360388
const storedVersion = localStorage.getItem(DATA_VERSION_KEY);
@@ -367,11 +395,11 @@ function loadInitialState(): GameState {
367395
const stored = localStorage.getItem(STORAGE_KEY);
368396
if (stored) {
369397
const parsed = JSON.parse(stored);
370-
return { ...parsed, players };
398+
return restoreRaccoonCards({ ...parsed, players });
371399
}
372-
return { ...DEFAULT_STATE, players };
400+
return restoreRaccoonCards({ ...DEFAULT_STATE, players });
373401
} catch {
374-
return DEFAULT_STATE;
402+
return restoreRaccoonCards(DEFAULT_STATE);
375403
}
376404
}
377405

0 commit comments

Comments
 (0)