create-rakta-app 0.1.8 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +962 -1068
- package/dist/index.js.map +8 -7
- package/package.json +3 -3
package/dist/index.js
CHANGED
|
@@ -1666,6 +1666,632 @@ function writeProjectFiles(projectRoot, generatedFiles) {
|
|
|
1666
1666
|
// src/generator.ts
|
|
1667
1667
|
import { readFileSync } from "fs";
|
|
1668
1668
|
|
|
1669
|
+
// src/starter.ts
|
|
1670
|
+
var STARTER_TYPES_CODE = `export interface SystemMetric {
|
|
1671
|
+
name: string;
|
|
1672
|
+
value: string | number;
|
|
1673
|
+
status: "nominal" | "warning" | "critical";
|
|
1674
|
+
}
|
|
1675
|
+
|
|
1676
|
+
export interface GameHighScore {
|
|
1677
|
+
name: string;
|
|
1678
|
+
score: number;
|
|
1679
|
+
date: string;
|
|
1680
|
+
}
|
|
1681
|
+
|
|
1682
|
+
export type AestheticUnit = "LENIS-MODERN" | "RETRO-CYBER" | "NEO-BRUTALIST";
|
|
1683
|
+
`;
|
|
1684
|
+
var STARTER_AUDIO_CODE = `let audioContext: AudioContext | null = null;
|
|
1685
|
+
|
|
1686
|
+
function getAudioContext(): AudioContext | null {
|
|
1687
|
+
if (typeof window === "undefined") return null;
|
|
1688
|
+
|
|
1689
|
+
const AudioContextCtor = window.AudioContext;
|
|
1690
|
+
if (!AudioContextCtor) return null;
|
|
1691
|
+
|
|
1692
|
+
audioContext ??= new AudioContextCtor();
|
|
1693
|
+
return audioContext;
|
|
1694
|
+
}
|
|
1695
|
+
|
|
1696
|
+
function playTone(frequency: number, durationMs: number, type: OscillatorType): void {
|
|
1697
|
+
const context = getAudioContext();
|
|
1698
|
+
if (!context) return;
|
|
1699
|
+
|
|
1700
|
+
const oscillator = context.createOscillator();
|
|
1701
|
+
const gain = context.createGain();
|
|
1702
|
+
|
|
1703
|
+
oscillator.type = type;
|
|
1704
|
+
oscillator.frequency.setValueAtTime(frequency, context.currentTime);
|
|
1705
|
+
gain.gain.setValueAtTime(0.045, context.currentTime);
|
|
1706
|
+
gain.gain.exponentialRampToValueAtTime(0.001, context.currentTime + durationMs / 1000);
|
|
1707
|
+
|
|
1708
|
+
oscillator.connect(gain);
|
|
1709
|
+
gain.connect(context.destination);
|
|
1710
|
+
oscillator.start();
|
|
1711
|
+
oscillator.stop(context.currentTime + durationMs / 1000);
|
|
1712
|
+
}
|
|
1713
|
+
|
|
1714
|
+
export function playJumpSound(): void {
|
|
1715
|
+
playTone(560, 90, "triangle");
|
|
1716
|
+
}
|
|
1717
|
+
|
|
1718
|
+
export function playScoreSound(): void {
|
|
1719
|
+
playTone(880, 120, "square");
|
|
1720
|
+
}
|
|
1721
|
+
`;
|
|
1722
|
+
var STARTER_SHRIMP_CHARACTER_CODE = `interface ShrimpCharacterProps {
|
|
1723
|
+
status: "IDLE" | "SWIMMING" | "JUMPING" | "DEAD";
|
|
1724
|
+
playerY: number;
|
|
1725
|
+
rotation?: number;
|
|
1726
|
+
}
|
|
1727
|
+
|
|
1728
|
+
export default function ShrimpCharacter({
|
|
1729
|
+
status,
|
|
1730
|
+
playerY,
|
|
1731
|
+
rotation,
|
|
1732
|
+
}: ShrimpCharacterProps) {
|
|
1733
|
+
const isDead = status === "DEAD";
|
|
1734
|
+
const isSwimming = status === "SWIMMING";
|
|
1735
|
+
const finalRotation = rotation ?? (isDead ? 180 : status === "JUMPING" ? -10 : 0);
|
|
1736
|
+
const shrimpColor = isDead ? "fill-orange-400" : "fill-rose-500";
|
|
1737
|
+
|
|
1738
|
+
return (
|
|
1739
|
+
<div
|
|
1740
|
+
className="relative flex h-16 w-16 select-none items-center justify-center transition-transform duration-75"
|
|
1741
|
+
style={{ transform: \`translateY(\${-playerY}px) rotate(\${finalRotation}deg)\` }}
|
|
1742
|
+
>
|
|
1743
|
+
<svg
|
|
1744
|
+
aria-label={isDead ? "Shrimp failed the run" : "Rakta shrimp runner"}
|
|
1745
|
+
role="img"
|
|
1746
|
+
viewBox="0 0 100 100"
|
|
1747
|
+
className={\`h-full w-full drop-shadow-[0_4px_12px_rgba(244,63,94,0.32)] \${isSwimming ? "animate-pulse" : ""}\`}
|
|
1748
|
+
>
|
|
1749
|
+
<path
|
|
1750
|
+
d="M 30 46 C 25 32, 45 22, 58 28 C 65 31, 62 48, 55 54 C 44 58, 35 55, 30 46 Z"
|
|
1751
|
+
className={shrimpColor}
|
|
1752
|
+
/>
|
|
1753
|
+
<path
|
|
1754
|
+
d="M 55 30 C 65 24, 76 34, 74 46 C 72 54, 62 55, 55 48 Z"
|
|
1755
|
+
className={shrimpColor}
|
|
1756
|
+
/>
|
|
1757
|
+
<path
|
|
1758
|
+
d="M 29 51 C 21 57, 16 61, 10 62"
|
|
1759
|
+
stroke={isDead ? "#fb923c" : "#f43f5e"}
|
|
1760
|
+
strokeWidth="4"
|
|
1761
|
+
strokeLinecap="round"
|
|
1762
|
+
fill="none"
|
|
1763
|
+
/>
|
|
1764
|
+
<path
|
|
1765
|
+
d="M 31 55 C 25 64, 22 70, 17 76"
|
|
1766
|
+
stroke={isDead ? "#fb923c" : "#f43f5e"}
|
|
1767
|
+
strokeWidth="3"
|
|
1768
|
+
strokeLinecap="round"
|
|
1769
|
+
fill="none"
|
|
1770
|
+
/>
|
|
1771
|
+
<circle cx="66" cy="38" r="4.5" fill={isDead ? "#18181b" : "#000"} />
|
|
1772
|
+
{!isDead && <circle cx="67.5" cy="36.5" r="1.2" fill="#fff" />}
|
|
1773
|
+
<path
|
|
1774
|
+
d="M 62 30 C 70 21, 82 19, 90 24"
|
|
1775
|
+
stroke="#fb7185"
|
|
1776
|
+
strokeWidth="2"
|
|
1777
|
+
strokeLinecap="round"
|
|
1778
|
+
fill="none"
|
|
1779
|
+
/>
|
|
1780
|
+
<path
|
|
1781
|
+
d="M 63 33 C 73 29, 84 31, 91 39"
|
|
1782
|
+
stroke="#fb7185"
|
|
1783
|
+
strokeWidth="2"
|
|
1784
|
+
strokeLinecap="round"
|
|
1785
|
+
fill="none"
|
|
1786
|
+
/>
|
|
1787
|
+
</svg>
|
|
1788
|
+
</div>
|
|
1789
|
+
);
|
|
1790
|
+
}
|
|
1791
|
+
`;
|
|
1792
|
+
var STARTER_CORAL_OBSTACLE_CODE = `interface CoralObstacleProps {
|
|
1793
|
+
position: "TOP" | "BOTTOM";
|
|
1794
|
+
height: number;
|
|
1795
|
+
width?: number;
|
|
1796
|
+
paletteIndex?: number;
|
|
1797
|
+
variant?: number;
|
|
1798
|
+
scaleX?: number;
|
|
1799
|
+
}
|
|
1800
|
+
|
|
1801
|
+
export default function CoralObstacle({
|
|
1802
|
+
position,
|
|
1803
|
+
height,
|
|
1804
|
+
width = 64,
|
|
1805
|
+
paletteIndex = 0,
|
|
1806
|
+
variant = 0,
|
|
1807
|
+
scaleX = 1,
|
|
1808
|
+
}: CoralObstacleProps) {
|
|
1809
|
+
const fallbackPalette = {
|
|
1810
|
+
primary: "#f43f5e",
|
|
1811
|
+
secondary: "#fda4af",
|
|
1812
|
+
shadow: "#9f1239",
|
|
1813
|
+
polyps: "#ffe4e6",
|
|
1814
|
+
name: "rose",
|
|
1815
|
+
};
|
|
1816
|
+
const palettes = [
|
|
1817
|
+
fallbackPalette,
|
|
1818
|
+
{ primary: "#06b6d4", secondary: "#67e8f9", shadow: "#155e75", polyps: "#ecfeff", name: "cyan" },
|
|
1819
|
+
{ primary: "#a855f7", secondary: "#d8b4fe", shadow: "#581c87", polyps: "#faf5ff", name: "amethyst" },
|
|
1820
|
+
{ primary: "#f59e0b", secondary: "#fde047", shadow: "#78350f", polyps: "#fefce8", name: "amber" },
|
|
1821
|
+
{ primary: "#10b981", secondary: "#6ee7b7", shadow: "#065f46", polyps: "#e6fffa", name: "emerald" },
|
|
1822
|
+
];
|
|
1823
|
+
|
|
1824
|
+
const palette = palettes[paletteIndex % palettes.length] ?? fallbackPalette;
|
|
1825
|
+
const coralId = \`coralGrad-\${palette.name}-\${variant}\`;
|
|
1826
|
+
const flipped = position === "TOP" ? "scale(1, -1) translate(0, -120)" : undefined;
|
|
1827
|
+
|
|
1828
|
+
return (
|
|
1829
|
+
<div
|
|
1830
|
+
className="absolute flex items-center justify-center pointer-events-none"
|
|
1831
|
+
style={{ height: \`\${height}px\`, width: \`\${width}px\`, transform: \`scaleX(\${scaleX})\` }}
|
|
1832
|
+
>
|
|
1833
|
+
<svg
|
|
1834
|
+
aria-hidden="true"
|
|
1835
|
+
focusable="false"
|
|
1836
|
+
viewBox="0 0 80 120"
|
|
1837
|
+
className="h-full w-full drop-shadow-[0_6px_14px_rgba(0,0,0,0.7)]"
|
|
1838
|
+
preserveAspectRatio="none"
|
|
1839
|
+
>
|
|
1840
|
+
<defs>
|
|
1841
|
+
<linearGradient id={coralId} x1="0%" y1="0%" x2="100%" y2="100%">
|
|
1842
|
+
<stop offset="0%" stopColor={palette.secondary} />
|
|
1843
|
+
<stop offset="52%" stopColor={palette.primary} />
|
|
1844
|
+
<stop offset="100%" stopColor={palette.shadow} />
|
|
1845
|
+
</linearGradient>
|
|
1846
|
+
<filter id={\`coralGlow-\${variant}\`}>
|
|
1847
|
+
<feGaussianBlur stdDeviation="1.8" result="coloredBlur" />
|
|
1848
|
+
<feMerge>
|
|
1849
|
+
<feMergeNode in="coloredBlur" />
|
|
1850
|
+
<feMergeNode in="SourceGraphic" />
|
|
1851
|
+
</feMerge>
|
|
1852
|
+
</filter>
|
|
1853
|
+
</defs>
|
|
1854
|
+
|
|
1855
|
+
<g transform={flipped} filter={\`url(#coralGlow-\${variant})\`}>
|
|
1856
|
+
<path
|
|
1857
|
+
d="M 35 120 C 30 100, 15 90, 10 70 C 5 50, 20 40, 15 25 C 10 10, 25 5, 30 15 C 35 30, 28 45, 38 65 Q 42 90, 40 120"
|
|
1858
|
+
fill={palette.shadow}
|
|
1859
|
+
opacity="0.85"
|
|
1860
|
+
/>
|
|
1861
|
+
<path
|
|
1862
|
+
d="M 45 120 C 50 105, 65 95, 70 75 C 75 55, 60 45, 65 30 C 70 15, 55 10, 50 20 C 45 35, 52 45, 42 65 Q 40 90, 45 120"
|
|
1863
|
+
fill={palette.shadow}
|
|
1864
|
+
opacity="0.85"
|
|
1865
|
+
/>
|
|
1866
|
+
<path
|
|
1867
|
+
d="M 40 120 C 35 90, 20 80, 24 55 C 28 30, 15 20, 22 10 C 29 0, 37 15, 35 30 C 33 45, 42 55, 40 75 C 38 95, 42 110, 40 120 Z"
|
|
1868
|
+
fill={\`url(#\${coralId})\`}
|
|
1869
|
+
/>
|
|
1870
|
+
<path
|
|
1871
|
+
d="M 40 120 C 45 95, 55 85, 52 65 C 49 45, 62 35, 58 20 C 54 5, 46 12, 48 25 C 50 38, 42 50, 44 70 C 46 90, 42 105, 40 120 Z"
|
|
1872
|
+
fill={\`url(#\${coralId})\`}
|
|
1873
|
+
/>
|
|
1874
|
+
<circle cx="22" cy="11" r="3.5" fill={palette.polyps} />
|
|
1875
|
+
<circle cx="58" cy="21" r="3.5" fill={palette.polyps} />
|
|
1876
|
+
<circle cx="30" cy="15" r="2.5" fill={palette.secondary} />
|
|
1877
|
+
<circle cx="50" cy="20" r="2.5" fill={palette.secondary} />
|
|
1878
|
+
</g>
|
|
1879
|
+
</svg>
|
|
1880
|
+
</div>
|
|
1881
|
+
);
|
|
1882
|
+
}
|
|
1883
|
+
`;
|
|
1884
|
+
var STARTER_PAGE_CODE = `import { useCallback, useEffect, useRef, useState } from "react";
|
|
1885
|
+
import type { IconType } from "react-icons";
|
|
1886
|
+
import {
|
|
1887
|
+
LuActivity,
|
|
1888
|
+
LuCode,
|
|
1889
|
+
LuCpu,
|
|
1890
|
+
LuGauge,
|
|
1891
|
+
LuRocket,
|
|
1892
|
+
LuShieldCheck,
|
|
1893
|
+
LuZap,
|
|
1894
|
+
} from "react-icons/lu";
|
|
1895
|
+
import CoralObstacle from "./components/CoralObstacle";
|
|
1896
|
+
import ShrimpCharacter from "./components/ShrimpCharacter";
|
|
1897
|
+
import type { AestheticUnit } from "./types";
|
|
1898
|
+
import { playJumpSound, playScoreSound } from "./utils/audio";
|
|
1899
|
+
|
|
1900
|
+
type GameStatus = "IDLE" | "SWIMMING" | "JUMPING" | "DEAD";
|
|
1901
|
+
type SimSpeed = "NORMAL" | "FAST" | "TURBO";
|
|
1902
|
+
type StatusCard = { title: string; text: string; Icon: IconType };
|
|
1903
|
+
type FeatureCard = { number: string; title: string; text: string; Icon: IconType };
|
|
1904
|
+
|
|
1905
|
+
const GAME_WIDTH = 720;
|
|
1906
|
+
const GAME_HEIGHT = 280;
|
|
1907
|
+
const SHRIMP_X = 82;
|
|
1908
|
+
const SHRIMP_SIZE = 58;
|
|
1909
|
+
const GRAVITY = 0.82;
|
|
1910
|
+
const JUMP_FORCE = 15;
|
|
1911
|
+
|
|
1912
|
+
function getSpeedValue(speed: SimSpeed): number {
|
|
1913
|
+
if (speed === "TURBO") return 8.2;
|
|
1914
|
+
if (speed === "FAST") return 6.4;
|
|
1915
|
+
return 5.2;
|
|
1916
|
+
}
|
|
1917
|
+
|
|
1918
|
+
const statusCards: StatusCard[] = [
|
|
1919
|
+
{ title: "Routing", text: "File-based routes with fast client navigation", Icon: LuZap },
|
|
1920
|
+
{ title: "Metadata", text: "Default title, favicon, and SEO shell included", Icon: LuShieldCheck },
|
|
1921
|
+
{ title: "Runtime", text: "Bun-powered dev server on localhost:3000", Icon: LuCpu },
|
|
1922
|
+
];
|
|
1923
|
+
|
|
1924
|
+
const featureCards: FeatureCard[] = [
|
|
1925
|
+
{ number: "01", title: "HOT RELOAD", text: "Pages, components, styles, layouts, stores, and route updates refresh immediately.", Icon: LuActivity },
|
|
1926
|
+
{ number: "02", title: "REACT ICONS", text: "Starter icons use react-icons/lu only. No lucide-react dependency ships.", Icon: LuCode },
|
|
1927
|
+
{ number: "03", title: "LOCAL FIRST", text: "The CLI prints and serves localhost:3000 for the frontend starter.", Icon: LuGauge },
|
|
1928
|
+
];
|
|
1929
|
+
|
|
1930
|
+
export default function WelcomePage() {
|
|
1931
|
+
const [status, setStatus] = useState<GameStatus>("IDLE");
|
|
1932
|
+
const [score, setScore] = useState(0);
|
|
1933
|
+
const [bestScore, setBestScore] = useState(() => {
|
|
1934
|
+
if (typeof localStorage === "undefined") return 0;
|
|
1935
|
+
return parseInt(localStorage.getItem("rakta_shrimprun_highscore") || "0", 10);
|
|
1936
|
+
});
|
|
1937
|
+
const [playerY, setPlayerY] = useState(0);
|
|
1938
|
+
const [rotation, setRotation] = useState(0);
|
|
1939
|
+
const [obstacleX, setObstacleX] = useState(GAME_WIDTH + 120);
|
|
1940
|
+
const [obstacleHeight, setObstacleHeight] = useState(84);
|
|
1941
|
+
const [obstacleWidth, setObstacleWidth] = useState(58);
|
|
1942
|
+
const [obstaclePalette, setObstaclePalette] = useState(0);
|
|
1943
|
+
const [obstacleVariant, setObstacleVariant] = useState(0);
|
|
1944
|
+
const [obstacleScaleX, setObstacleScaleX] = useState(1);
|
|
1945
|
+
const [aestheticUnit, setAestheticUnit] = useState<AestheticUnit>("LENIS-MODERN");
|
|
1946
|
+
const [simSpeed, setSimSpeed] = useState<SimSpeed>("NORMAL");
|
|
1947
|
+
const [lowLatencyMode, setLowLatencyMode] = useState(true);
|
|
1948
|
+
const [configToast, setConfigToast] = useState<string | null>(null);
|
|
1949
|
+
|
|
1950
|
+
const statusRef = useRef(status);
|
|
1951
|
+
const velocityRef = useRef(0);
|
|
1952
|
+
const playerYRef = useRef(0);
|
|
1953
|
+
const obstacleXRef = useRef(GAME_WIDTH + 120);
|
|
1954
|
+
const scoreRef = useRef(0);
|
|
1955
|
+
const lastTimeRef = useRef(0);
|
|
1956
|
+
const animationRef = useRef<number | null>(null);
|
|
1957
|
+
const toastTimerRef = useRef<number | null>(null);
|
|
1958
|
+
|
|
1959
|
+
useEffect(() => {
|
|
1960
|
+
statusRef.current = status;
|
|
1961
|
+
}, [status]);
|
|
1962
|
+
|
|
1963
|
+
const showConfigToast = useCallback((message: string) => {
|
|
1964
|
+
setConfigToast(message);
|
|
1965
|
+
if (toastTimerRef.current !== null) {
|
|
1966
|
+
window.clearTimeout(toastTimerRef.current);
|
|
1967
|
+
}
|
|
1968
|
+
toastTimerRef.current = window.setTimeout(() => setConfigToast(null), 1400);
|
|
1969
|
+
}, []);
|
|
1970
|
+
|
|
1971
|
+
const resetObstacle = useCallback(() => {
|
|
1972
|
+
obstacleXRef.current = GAME_WIDTH + 80 + Math.random() * 220;
|
|
1973
|
+
setObstacleX(obstacleXRef.current);
|
|
1974
|
+
setObstacleHeight(58 + Math.floor(Math.random() * 82));
|
|
1975
|
+
setObstacleWidth(42 + Math.floor(Math.random() * 32));
|
|
1976
|
+
setObstaclePalette((value) => (value + 1) % 5);
|
|
1977
|
+
setObstacleVariant((value) => (value + 1) % 3);
|
|
1978
|
+
setObstacleScaleX(Math.random() > 0.5 ? 1 : -1);
|
|
1979
|
+
}, []);
|
|
1980
|
+
|
|
1981
|
+
const startRun = useCallback(() => {
|
|
1982
|
+
if (statusRef.current === "DEAD") {
|
|
1983
|
+
scoreRef.current = 0;
|
|
1984
|
+
setScore(0);
|
|
1985
|
+
playerYRef.current = 0;
|
|
1986
|
+
velocityRef.current = 0;
|
|
1987
|
+
setPlayerY(0);
|
|
1988
|
+
resetObstacle();
|
|
1989
|
+
}
|
|
1990
|
+
|
|
1991
|
+
statusRef.current = "SWIMMING";
|
|
1992
|
+
setStatus("SWIMMING");
|
|
1993
|
+
}, [resetObstacle]);
|
|
1994
|
+
|
|
1995
|
+
const jump = useCallback(() => {
|
|
1996
|
+
if (statusRef.current === "IDLE" || statusRef.current === "DEAD") {
|
|
1997
|
+
startRun();
|
|
1998
|
+
}
|
|
1999
|
+
|
|
2000
|
+
if (playerYRef.current <= 4) {
|
|
2001
|
+
velocityRef.current = JUMP_FORCE;
|
|
2002
|
+
setStatus("JUMPING");
|
|
2003
|
+
playJumpSound();
|
|
2004
|
+
}
|
|
2005
|
+
}, [startRun]);
|
|
2006
|
+
|
|
2007
|
+
useEffect(() => {
|
|
2008
|
+
function onKeyDown(event: KeyboardEvent): void {
|
|
2009
|
+
if (event.code === "Space") {
|
|
2010
|
+
event.preventDefault();
|
|
2011
|
+
jump();
|
|
2012
|
+
}
|
|
2013
|
+
}
|
|
2014
|
+
|
|
2015
|
+
window.addEventListener("keydown", onKeyDown);
|
|
2016
|
+
return () => window.removeEventListener("keydown", onKeyDown);
|
|
2017
|
+
}, [jump]);
|
|
2018
|
+
|
|
2019
|
+
useEffect(() => {
|
|
2020
|
+
function frame(timestamp: number): void {
|
|
2021
|
+
const delta = Math.min(timestamp - lastTimeRef.current, 32) / 16.67;
|
|
2022
|
+
lastTimeRef.current = timestamp;
|
|
2023
|
+
|
|
2024
|
+
if (statusRef.current === "SWIMMING" || statusRef.current === "JUMPING") {
|
|
2025
|
+
velocityRef.current -= GRAVITY * delta;
|
|
2026
|
+
playerYRef.current = Math.max(0, playerYRef.current + velocityRef.current * delta);
|
|
2027
|
+
|
|
2028
|
+
if (playerYRef.current === 0 && velocityRef.current < 0) {
|
|
2029
|
+
velocityRef.current = 0;
|
|
2030
|
+
setStatus("SWIMMING");
|
|
2031
|
+
}
|
|
2032
|
+
|
|
2033
|
+
obstacleXRef.current -= getSpeedValue(simSpeed) * (lowLatencyMode ? 1.08 : 0.92) * delta;
|
|
2034
|
+
|
|
2035
|
+
if (obstacleXRef.current < -90) {
|
|
2036
|
+
resetObstacle();
|
|
2037
|
+
scoreRef.current += 7;
|
|
2038
|
+
setScore(scoreRef.current);
|
|
2039
|
+
playScoreSound();
|
|
2040
|
+
}
|
|
2041
|
+
|
|
2042
|
+
scoreRef.current += 1;
|
|
2043
|
+
setScore(scoreRef.current);
|
|
2044
|
+
|
|
2045
|
+
const shrimpLeft = SHRIMP_X + 8;
|
|
2046
|
+
const shrimpRight = SHRIMP_X + SHRIMP_SIZE - 8;
|
|
2047
|
+
const shrimpBottom = GAME_HEIGHT - 54 - playerYRef.current;
|
|
2048
|
+
const shrimpTop = shrimpBottom - SHRIMP_SIZE + 12;
|
|
2049
|
+
const obstacleLeft = obstacleXRef.current + 8;
|
|
2050
|
+
const obstacleRight = obstacleXRef.current + obstacleWidth - 8;
|
|
2051
|
+
const obstacleTop = GAME_HEIGHT - 48 - obstacleHeight;
|
|
2052
|
+
const obstacleBottom = GAME_HEIGHT - 48;
|
|
2053
|
+
const didCollide =
|
|
2054
|
+
shrimpLeft < obstacleRight &&
|
|
2055
|
+
shrimpRight > obstacleLeft &&
|
|
2056
|
+
shrimpTop < obstacleBottom &&
|
|
2057
|
+
shrimpBottom > obstacleTop;
|
|
2058
|
+
|
|
2059
|
+
if (didCollide) {
|
|
2060
|
+
statusRef.current = "DEAD";
|
|
2061
|
+
setStatus("DEAD");
|
|
2062
|
+
setRotation(180);
|
|
2063
|
+
const nextBest = Math.max(bestScore, scoreRef.current);
|
|
2064
|
+
setBestScore(nextBest);
|
|
2065
|
+
localStorage.setItem("rakta_shrimprun_highscore", String(nextBest));
|
|
2066
|
+
} else {
|
|
2067
|
+
setRotation(playerYRef.current > 0 ? -8 : 0);
|
|
2068
|
+
}
|
|
2069
|
+
|
|
2070
|
+
setPlayerY(playerYRef.current);
|
|
2071
|
+
setObstacleX(obstacleXRef.current);
|
|
2072
|
+
}
|
|
2073
|
+
|
|
2074
|
+
animationRef.current = requestAnimationFrame(frame);
|
|
2075
|
+
}
|
|
2076
|
+
|
|
2077
|
+
animationRef.current = requestAnimationFrame(frame);
|
|
2078
|
+
return () => {
|
|
2079
|
+
if (animationRef.current !== null) cancelAnimationFrame(animationRef.current);
|
|
2080
|
+
};
|
|
2081
|
+
}, [bestScore, lowLatencyMode, obstacleHeight, obstacleWidth, resetObstacle, simSpeed]);
|
|
2082
|
+
|
|
2083
|
+
const obstacleSizeClass =
|
|
2084
|
+
obstacleHeight < 82 ? "KECIL" : obstacleHeight < 120 ? "SEDANG" : "BESAR";
|
|
2085
|
+
const obstaclePos = "BOTTOM" as const;
|
|
2086
|
+
|
|
2087
|
+
return (
|
|
2088
|
+
<div className="rakta-welcome min-h-screen bg-[#050505] text-white">
|
|
2089
|
+
<div className="scanline" />
|
|
2090
|
+
<main className="rakta-shell mx-auto flex w-full max-w-7xl flex-col gap-10 px-6 py-8 md:px-10">
|
|
2091
|
+
<nav className="rakta-nav flex items-center justify-between border-b border-surface-stroke pb-5">
|
|
2092
|
+
<div className="flex items-center gap-3 font-mono text-sm font-extrabold tracking-tight">
|
|
2093
|
+
<span className="grid h-8 w-8 place-items-center border border-brand-pink bg-brand-pink text-black">
|
|
2094
|
+
R
|
|
2095
|
+
</span>
|
|
2096
|
+
Rakta.js
|
|
2097
|
+
</div>
|
|
2098
|
+
<div className="hidden items-center gap-6 font-mono text-[11px] font-bold uppercase tracking-wider text-[#b5b5b5] md:flex">
|
|
2099
|
+
<a href="#features" className="transition hover:text-white">Features</a>
|
|
2100
|
+
<a href="#shrimprun" className="transition hover:text-white">ShrimpRun</a>
|
|
2101
|
+
<a href="#start" className="transition hover:text-white">Start</a>
|
|
2102
|
+
</div>
|
|
2103
|
+
</nav>
|
|
2104
|
+
|
|
2105
|
+
<section className="rakta-hero grid min-h-[540px] items-center gap-10 border-b border-surface-stroke py-12 md:grid-cols-[1.08fr_0.92fr]">
|
|
2106
|
+
<div>
|
|
2107
|
+
<p className="mb-5 font-mono text-[11px] font-bold uppercase tracking-[0.32em] text-brand-pink">
|
|
2108
|
+
THE RED ROUTER FRAMEWORK
|
|
2109
|
+
</p>
|
|
2110
|
+
<h1 className="max-w-4xl text-6xl font-black uppercase leading-[0.88] tracking-normal text-white md:text-8xl">
|
|
2111
|
+
Small in size. Fierce in speed.
|
|
2112
|
+
</h1>
|
|
2113
|
+
<p className="mt-7 max-w-2xl text-sm leading-7 text-[#b5b5b5] md:text-base">
|
|
2114
|
+
Rakta.js is ready. React, Bun, file routes, hot reload, metadata, favicon,
|
|
2115
|
+
and the ShrimpRun starter are already wired for your first route.
|
|
2116
|
+
</p>
|
|
2117
|
+
<div className="rakta-actions mt-8 flex flex-wrap gap-3">
|
|
2118
|
+
<a
|
|
2119
|
+
href="#shrimprun"
|
|
2120
|
+
className="inline-flex h-11 items-center gap-2 border border-brand-pink bg-brand-pink px-5 font-mono text-xs font-extrabold uppercase text-white transition hover:bg-white hover:text-black"
|
|
2121
|
+
>
|
|
2122
|
+
<LuRocket className="h-4 w-4" />
|
|
2123
|
+
Play ShrimpRun
|
|
2124
|
+
</a>
|
|
2125
|
+
<a
|
|
2126
|
+
href="#start"
|
|
2127
|
+
className="inline-flex h-11 items-center gap-2 border border-surface-stroke px-5 font-mono text-xs font-extrabold uppercase text-white transition hover:border-white"
|
|
2128
|
+
>
|
|
2129
|
+
<LuCode className="h-4 w-4" />
|
|
2130
|
+
Start Building
|
|
2131
|
+
</a>
|
|
2132
|
+
</div>
|
|
2133
|
+
</div>
|
|
2134
|
+
|
|
2135
|
+
<div className="rakta-status-grid grid gap-3 border border-surface-stroke bg-surface-card p-5">
|
|
2136
|
+
{statusCards.map(({ title, text, Icon }) => (
|
|
2137
|
+
<div key={title} className="flex gap-4 border border-white/5 bg-black p-4">
|
|
2138
|
+
<Icon className="mt-1 h-5 w-5 text-brand-pink" />
|
|
2139
|
+
<div>
|
|
2140
|
+
<h2 className="font-mono text-xs font-extrabold uppercase">{title}</h2>
|
|
2141
|
+
<p className="mt-1 text-xs leading-5 text-[#b5b5b5]">{text}</p>
|
|
2142
|
+
</div>
|
|
2143
|
+
</div>
|
|
2144
|
+
))}
|
|
2145
|
+
</div>
|
|
2146
|
+
</section>
|
|
2147
|
+
|
|
2148
|
+
<section id="features" className="rakta-feature-grid grid grid-cols-1 gap-0 border border-surface-stroke md:grid-cols-3">
|
|
2149
|
+
{featureCards.map(({ number, title, text, Icon }) => (
|
|
2150
|
+
<div key={title} className="border-b border-surface-stroke p-7 md:border-b-0 md:border-r last:md:border-r-0">
|
|
2151
|
+
<span className="font-mono text-[10px] font-bold text-brand-pink">{number}</span>
|
|
2152
|
+
<Icon className="my-8 h-6 w-6 text-white" />
|
|
2153
|
+
<h2 className="font-mono text-xl font-extrabold uppercase">{title}</h2>
|
|
2154
|
+
<p className="mt-3 text-xs leading-6 text-[#b5b5b5]">{text}</p>
|
|
2155
|
+
</div>
|
|
2156
|
+
))}
|
|
2157
|
+
</section>
|
|
2158
|
+
|
|
2159
|
+
<section id="shrimprun" className="rakta-game-section grid gap-5">
|
|
2160
|
+
<div>
|
|
2161
|
+
<p className="font-mono text-[10px] font-bold uppercase tracking-[0.28em] text-brand-pink">
|
|
2162
|
+
SHRIMPRUN SIMULATION
|
|
2163
|
+
</p>
|
|
2164
|
+
<h2 className="mt-3 text-4xl font-black uppercase md:text-6xl">
|
|
2165
|
+
Avoid the coral.
|
|
2166
|
+
</h2>
|
|
2167
|
+
</div>
|
|
2168
|
+
|
|
2169
|
+
<button
|
|
2170
|
+
type="button"
|
|
2171
|
+
onClick={jump}
|
|
2172
|
+
className="rakta-game-field relative h-[280px] overflow-hidden border-2 border-surface-stroke bg-black text-left outline-none transition focus-visible:border-brand-pink"
|
|
2173
|
+
aria-label="ShrimpRun game area. Press Space or click to jump."
|
|
2174
|
+
>
|
|
2175
|
+
<div className="bg-grid-glow absolute inset-0 opacity-80" />
|
|
2176
|
+
<div className="absolute inset-x-0 bottom-0 h-12 border-t border-cyan-400/20 bg-cyan-400/5" />
|
|
2177
|
+
<div className="seaweed-waving-left-1 absolute bottom-8 left-9 h-16 w-3 bg-emerald-500/70" />
|
|
2178
|
+
<div className="seaweed-waving-left-2 absolute bottom-8 left-16 h-24 w-3 bg-cyan-500/60" />
|
|
2179
|
+
<div className="seaweed-waving-right-1 absolute bottom-8 right-20 h-20 w-3 bg-emerald-500/70" />
|
|
2180
|
+
|
|
2181
|
+
<div className="absolute left-6 top-5 z-20 flex gap-4 font-mono text-[11px] font-bold uppercase">
|
|
2182
|
+
<span>Score: {score}</span>
|
|
2183
|
+
<span className="text-brand-pink">Best: {bestScore}</span>
|
|
2184
|
+
<span>{status}</span>
|
|
2185
|
+
</div>
|
|
2186
|
+
|
|
2187
|
+
<div
|
|
2188
|
+
className="absolute z-20"
|
|
2189
|
+
style={{ left: SHRIMP_X, bottom: 48 + playerY }}
|
|
2190
|
+
>
|
|
2191
|
+
<ShrimpCharacter status={status} playerY={0} rotation={rotation} />
|
|
2192
|
+
</div>
|
|
2193
|
+
|
|
2194
|
+
<div className="absolute z-10" style={{ left: obstacleX, bottom: 48 }}>
|
|
2195
|
+
<CoralObstacle
|
|
2196
|
+
position={obstaclePos}
|
|
2197
|
+
height={obstacleHeight}
|
|
2198
|
+
width={obstacleWidth}
|
|
2199
|
+
paletteIndex={obstaclePalette}
|
|
2200
|
+
variant={obstacleVariant}
|
|
2201
|
+
scaleX={obstacleScaleX}
|
|
2202
|
+
/>
|
|
2203
|
+
</div>
|
|
2204
|
+
|
|
2205
|
+
{status === "IDLE" && (
|
|
2206
|
+
<div className="absolute inset-0 z-30 grid place-items-center bg-black/55 font-mono text-xs font-bold uppercase tracking-[0.2em] text-white">
|
|
2207
|
+
Press Space or click to start
|
|
2208
|
+
</div>
|
|
2209
|
+
)}
|
|
2210
|
+
{status === "DEAD" && (
|
|
2211
|
+
<div className="absolute inset-0 z-30 grid place-items-center bg-black/75 text-center font-mono">
|
|
2212
|
+
<div>
|
|
2213
|
+
<p className="text-3xl font-black text-brand-pink">SYSTEM IMPACT</p>
|
|
2214
|
+
<p className="mt-2 text-xs uppercase text-[#b5b5b5]">Click to reboot the run</p>
|
|
2215
|
+
</div>
|
|
2216
|
+
</div>
|
|
2217
|
+
)}
|
|
2218
|
+
</button>
|
|
2219
|
+
|
|
2220
|
+
<div className="rakta-game-controls flex flex-col justify-between gap-4 font-mono text-[10px] text-zinc-500 md:flex-row md:items-center">
|
|
2221
|
+
<div className="flex flex-wrap items-center gap-3">
|
|
2222
|
+
<span className="font-extrabold text-white">SHRIMPRUN {aestheticUnit.replace("-", " ")}</span>
|
|
2223
|
+
<span>CORAL:</span>
|
|
2224
|
+
<span className="border border-brand-pink/30 bg-brand-pink/5 px-2 py-0.5 font-bold text-brand-pink">
|
|
2225
|
+
{obstacleSizeClass}
|
|
2226
|
+
</span>
|
|
2227
|
+
</div>
|
|
2228
|
+
|
|
2229
|
+
<div className="flex flex-wrap items-center gap-3">
|
|
2230
|
+
{(["LENIS-MODERN", "RETRO-CYBER", "NEO-BRUTALIST"] as AestheticUnit[]).map((unit) => (
|
|
2231
|
+
<button
|
|
2232
|
+
type="button"
|
|
2233
|
+
key={unit}
|
|
2234
|
+
onClick={() => {
|
|
2235
|
+
setAestheticUnit(unit);
|
|
2236
|
+
showConfigToast(\`STYLE SET: \${unit}\`);
|
|
2237
|
+
playScoreSound();
|
|
2238
|
+
}}
|
|
2239
|
+
className={\`px-2 py-1 font-bold transition \${aestheticUnit === unit ? "bg-white text-black" : "border border-surface-stroke text-zinc-500 hover:text-white"}\`}
|
|
2240
|
+
>
|
|
2241
|
+
{unit}
|
|
2242
|
+
</button>
|
|
2243
|
+
))}
|
|
2244
|
+
{(["NORMAL", "FAST", "TURBO"] as const).map((speed) => (
|
|
2245
|
+
<button
|
|
2246
|
+
type="button"
|
|
2247
|
+
key={speed}
|
|
2248
|
+
onClick={() => {
|
|
2249
|
+
setSimSpeed(speed);
|
|
2250
|
+
showConfigToast(\`SPEED SET: \${speed}\`);
|
|
2251
|
+
playScoreSound();
|
|
2252
|
+
}}
|
|
2253
|
+
className={\`px-2 py-1 font-bold transition \${simSpeed === speed ? "bg-brand-pink text-white" : "border border-surface-stroke text-zinc-500 hover:text-white"}\`}
|
|
2254
|
+
>
|
|
2255
|
+
{speed}
|
|
2256
|
+
</button>
|
|
2257
|
+
))}
|
|
2258
|
+
<button
|
|
2259
|
+
type="button"
|
|
2260
|
+
onClick={() => {
|
|
2261
|
+
setLowLatencyMode((value) => !value);
|
|
2262
|
+
showConfigToast(\`LATENCY MODE: \${!lowLatencyMode ? "LOW" : "STANDARD"}\`);
|
|
2263
|
+
playJumpSound();
|
|
2264
|
+
}}
|
|
2265
|
+
className={\`px-2 py-1 font-bold transition \${lowLatencyMode ? "bg-cyan-400 text-black" : "border border-surface-stroke text-zinc-500"}\`}
|
|
2266
|
+
>
|
|
2267
|
+
LOW LATENCY {lowLatencyMode ? "ON" : "OFF"}
|
|
2268
|
+
</button>
|
|
2269
|
+
</div>
|
|
2270
|
+
</div>
|
|
2271
|
+
|
|
2272
|
+
{configToast && (
|
|
2273
|
+
<div className="fixed bottom-8 left-1/2 z-50 -translate-x-1/2 border border-brand-pink bg-black px-4 py-2 font-mono text-[10px] font-bold uppercase tracking-widest text-brand-pink">
|
|
2274
|
+
{configToast}
|
|
2275
|
+
</div>
|
|
2276
|
+
)}
|
|
2277
|
+
</section>
|
|
2278
|
+
|
|
2279
|
+
<section id="start" className="rakta-start border-t border-surface-stroke py-10">
|
|
2280
|
+
<p className="font-mono text-[10px] font-bold uppercase tracking-[0.28em] text-brand-pink">
|
|
2281
|
+
NEXT MOVE
|
|
2282
|
+
</p>
|
|
2283
|
+
<h2 className="mt-3 text-3xl font-black uppercase md:text-5xl">Edit app/page and ship.</h2>
|
|
2284
|
+
<p className="mt-4 max-w-2xl text-sm leading-7 text-[#b5b5b5]">
|
|
2285
|
+
This starter is intentionally immediate: no popups, no placeholder flow,
|
|
2286
|
+
and no hidden modal dependencies. Your first route is already alive.
|
|
2287
|
+
</p>
|
|
2288
|
+
</section>
|
|
2289
|
+
</main>
|
|
2290
|
+
</div>
|
|
2291
|
+
);
|
|
2292
|
+
}
|
|
2293
|
+
`;
|
|
2294
|
+
|
|
1669
2295
|
// src/types.ts
|
|
1670
2296
|
var CSS_DISPLAY = {
|
|
1671
2297
|
tailwind: "Tailwind CSS",
|
|
@@ -1703,14 +2329,18 @@ var PROJECT_MODE_DISPLAY = {
|
|
|
1703
2329
|
fullstack: "Fullstack app (frontend + backend + database)",
|
|
1704
2330
|
"frontend-only": "Frontend only (no backend, no database)"
|
|
1705
2331
|
};
|
|
2332
|
+
var PROJECT_LANGUAGE_DISPLAY = {
|
|
2333
|
+
typescript: "TypeScript / TSX",
|
|
2334
|
+
javascript: "JavaScript / JSX"
|
|
2335
|
+
};
|
|
1706
2336
|
|
|
1707
2337
|
// src/generator.ts
|
|
1708
2338
|
var DEFAULT_METADATA_TITLE = "Rakta.js | Small in size. Fierce in speed. Alive in every route";
|
|
1709
2339
|
var FAVICON_BYTES = readFileSync(new URL("../assets/favicon.ico", import.meta.url));
|
|
1710
2340
|
function getRootFiles(projectConfig) {
|
|
1711
|
-
const { projectName, projectMode } = projectConfig;
|
|
2341
|
+
const { projectName, projectMode, useTypeScript } = projectConfig;
|
|
1712
2342
|
const workspaces = projectMode === "fullstack" ? ["frontend", "backend", "shared"] : [];
|
|
1713
|
-
|
|
2343
|
+
const files = [
|
|
1714
2344
|
{
|
|
1715
2345
|
path: "package.json",
|
|
1716
2346
|
content: JSON.stringify({
|
|
@@ -1725,36 +2355,16 @@ function getRootFiles(projectConfig) {
|
|
|
1725
2355
|
"build:backend": "cd backend && bun run build",
|
|
1726
2356
|
build: "bun run build:frontend && bun run build:backend",
|
|
1727
2357
|
start: "cd backend && bun run start",
|
|
1728
|
-
|
|
2358
|
+
...useTypeScript ? {
|
|
2359
|
+
typecheck: "cd frontend && bun run typecheck && cd ../backend && bun run typecheck"
|
|
2360
|
+
} : {}
|
|
1729
2361
|
} : {
|
|
1730
2362
|
dev: "rakta dev",
|
|
1731
2363
|
build: "rakta build",
|
|
1732
2364
|
start: "rakta start",
|
|
1733
|
-
typecheck: "tsc --noEmit"
|
|
1734
|
-
},
|
|
1735
|
-
description: `${projectName} \u2014 built with Rakta.js`
|
|
1736
|
-
}, null, 2)
|
|
1737
|
-
},
|
|
1738
|
-
{
|
|
1739
|
-
path: "tsconfig.base.json",
|
|
1740
|
-
content: JSON.stringify({
|
|
1741
|
-
compilerOptions: {
|
|
1742
|
-
target: "ESNext",
|
|
1743
|
-
module: "ESNext",
|
|
1744
|
-
moduleResolution: "Bundler",
|
|
1745
|
-
jsx: "react-jsx",
|
|
1746
|
-
lib: ["ESNext", "DOM", "DOM.Iterable"],
|
|
1747
|
-
strict: true,
|
|
1748
|
-
noUncheckedIndexedAccess: true,
|
|
1749
|
-
exactOptionalPropertyTypes: true,
|
|
1750
|
-
skipLibCheck: true,
|
|
1751
|
-
esModuleInterop: true,
|
|
1752
|
-
allowSyntheticDefaultImports: true,
|
|
1753
|
-
resolveJsonModule: true,
|
|
1754
|
-
verbatimModuleSyntax: true,
|
|
1755
|
-
isolatedModules: true
|
|
2365
|
+
...useTypeScript ? { typecheck: "tsc --noEmit" } : {}
|
|
1756
2366
|
},
|
|
1757
|
-
|
|
2367
|
+
description: `${projectName} \xE2\u20AC\u201D built with Rakta.js`
|
|
1758
2368
|
}, null, 2)
|
|
1759
2369
|
},
|
|
1760
2370
|
{
|
|
@@ -1799,11 +2409,38 @@ dist/
|
|
|
1799
2409
|
content: generateProjectReadme(projectConfig)
|
|
1800
2410
|
}
|
|
1801
2411
|
];
|
|
2412
|
+
if (useTypeScript) {
|
|
2413
|
+
files.splice(1, 0, {
|
|
2414
|
+
path: "tsconfig.base.json",
|
|
2415
|
+
content: JSON.stringify({
|
|
2416
|
+
compilerOptions: {
|
|
2417
|
+
target: "ESNext",
|
|
2418
|
+
module: "ESNext",
|
|
2419
|
+
moduleResolution: "Bundler",
|
|
2420
|
+
jsx: "react-jsx",
|
|
2421
|
+
lib: ["ESNext", "DOM", "DOM.Iterable"],
|
|
2422
|
+
strict: true,
|
|
2423
|
+
noUncheckedIndexedAccess: true,
|
|
2424
|
+
exactOptionalPropertyTypes: true,
|
|
2425
|
+
skipLibCheck: true,
|
|
2426
|
+
esModuleInterop: true,
|
|
2427
|
+
allowSyntheticDefaultImports: true,
|
|
2428
|
+
resolveJsonModule: true,
|
|
2429
|
+
verbatimModuleSyntax: true,
|
|
2430
|
+
isolatedModules: true
|
|
2431
|
+
},
|
|
2432
|
+
exclude: ["node_modules", "dist", "**/dist/**"]
|
|
2433
|
+
}, null, 2)
|
|
2434
|
+
});
|
|
2435
|
+
}
|
|
2436
|
+
return files;
|
|
1802
2437
|
}
|
|
1803
2438
|
function getFrontendOnlyFiles(projectConfig) {
|
|
1804
|
-
const { projectName, cssFramework } = projectConfig;
|
|
2439
|
+
const { projectName, cssFramework, useTypeScript } = projectConfig;
|
|
1805
2440
|
const styleFileName = cssFramework === "sass" ? "globals.scss" : "globals.css";
|
|
1806
|
-
|
|
2441
|
+
const pageExtension = useTypeScript ? "tsx" : "jsx";
|
|
2442
|
+
const scriptExtension = useTypeScript ? "ts" : "js";
|
|
2443
|
+
const files = [
|
|
1807
2444
|
{
|
|
1808
2445
|
path: "package.json",
|
|
1809
2446
|
content: JSON.stringify({
|
|
@@ -1816,47 +2453,27 @@ function getFrontendOnlyFiles(projectConfig) {
|
|
|
1816
2453
|
build: "rakta build",
|
|
1817
2454
|
start: "rakta start",
|
|
1818
2455
|
routes: "rakta routes",
|
|
1819
|
-
typecheck: "tsc --noEmit"
|
|
2456
|
+
...useTypeScript ? { typecheck: "tsc --noEmit" } : {}
|
|
1820
2457
|
},
|
|
1821
2458
|
dependencies: {
|
|
1822
|
-
raktajs: "^0.
|
|
2459
|
+
raktajs: "^0.2.0",
|
|
1823
2460
|
react: "^19.2.7",
|
|
1824
2461
|
"react-dom": "^19.2.7",
|
|
2462
|
+
"react-icons": "^5.7.0",
|
|
1825
2463
|
...getCssDependencies(cssFramework)
|
|
1826
2464
|
},
|
|
1827
|
-
devDependencies: {
|
|
2465
|
+
devDependencies: useTypeScript ? {
|
|
1828
2466
|
"@types/react": "^19.2.17",
|
|
1829
2467
|
"@types/react-dom": "^19.2.3",
|
|
1830
2468
|
typescript: "^6.0.3",
|
|
1831
2469
|
...getCssDevDependencies(cssFramework)
|
|
2470
|
+
} : {
|
|
2471
|
+
...getCssDevDependencies(cssFramework)
|
|
1832
2472
|
}
|
|
1833
2473
|
}, null, 2)
|
|
1834
2474
|
},
|
|
1835
2475
|
{
|
|
1836
|
-
path:
|
|
1837
|
-
content: JSON.stringify({
|
|
1838
|
-
extends: "./tsconfig.base.json",
|
|
1839
|
-
compilerOptions: {
|
|
1840
|
-
outDir: "./dist",
|
|
1841
|
-
rootDir: "./",
|
|
1842
|
-
types: ["react", "react-dom"]
|
|
1843
|
-
},
|
|
1844
|
-
include: [
|
|
1845
|
-
"rakta-env.d.ts",
|
|
1846
|
-
"app/**/*",
|
|
1847
|
-
"components/**/*",
|
|
1848
|
-
"styles/**/*",
|
|
1849
|
-
"rakta.config.ts"
|
|
1850
|
-
],
|
|
1851
|
-
exclude: ["node_modules", "dist"]
|
|
1852
|
-
}, null, 2)
|
|
1853
|
-
},
|
|
1854
|
-
{
|
|
1855
|
-
path: "rakta-env.d.ts",
|
|
1856
|
-
content: generateFrontendOnlyRaktaEnv()
|
|
1857
|
-
},
|
|
1858
|
-
{
|
|
1859
|
-
path: "rakta.config.ts",
|
|
2476
|
+
path: `rakta.config.${scriptExtension}`,
|
|
1860
2477
|
content: `import { defineRaktaConfig } from "raktajs";
|
|
1861
2478
|
|
|
1862
2479
|
export default defineRaktaConfig({
|
|
@@ -1873,32 +2490,40 @@ export default defineRaktaConfig({
|
|
|
1873
2490
|
`
|
|
1874
2491
|
},
|
|
1875
2492
|
{
|
|
1876
|
-
path:
|
|
2493
|
+
path: `app/layout.${pageExtension}`,
|
|
1877
2494
|
content: generateFrontendOnlyLayout()
|
|
1878
2495
|
},
|
|
1879
2496
|
{
|
|
1880
|
-
path:
|
|
2497
|
+
path: `app/page.${pageExtension}`,
|
|
1881
2498
|
content: generateFrontendOnlyPage(projectName)
|
|
1882
2499
|
},
|
|
1883
2500
|
{
|
|
1884
|
-
path:
|
|
2501
|
+
path: `app/loading.${pageExtension}`,
|
|
1885
2502
|
content: generateFrontendOnlyLoading()
|
|
1886
2503
|
},
|
|
1887
2504
|
{
|
|
1888
|
-
path:
|
|
2505
|
+
path: `app/error.${pageExtension}`,
|
|
1889
2506
|
content: generateFrontendOnlyError()
|
|
1890
2507
|
},
|
|
1891
2508
|
{
|
|
1892
|
-
path:
|
|
2509
|
+
path: `app/notFound.${pageExtension}`,
|
|
1893
2510
|
content: generateFrontendOnlyNotFound()
|
|
1894
2511
|
},
|
|
1895
2512
|
{
|
|
1896
|
-
path:
|
|
1897
|
-
content:
|
|
2513
|
+
path: `app/components/CoralObstacle.${pageExtension}`,
|
|
2514
|
+
content: STARTER_CORAL_OBSTACLE_CODE
|
|
2515
|
+
},
|
|
2516
|
+
{
|
|
2517
|
+
path: `app/components/ShrimpCharacter.${pageExtension}`,
|
|
2518
|
+
content: STARTER_SHRIMP_CHARACTER_CODE
|
|
2519
|
+
},
|
|
2520
|
+
{
|
|
2521
|
+
path: `app/utils/audio.${scriptExtension}`,
|
|
2522
|
+
content: STARTER_AUDIO_CODE
|
|
1898
2523
|
},
|
|
1899
2524
|
{
|
|
1900
|
-
path:
|
|
1901
|
-
content:
|
|
2525
|
+
path: `app/types.${scriptExtension}`,
|
|
2526
|
+
content: STARTER_TYPES_CODE
|
|
1902
2527
|
},
|
|
1903
2528
|
{
|
|
1904
2529
|
path: `styles/${styleFileName}`,
|
|
@@ -1913,6 +2538,31 @@ export default defineRaktaConfig({
|
|
|
1913
2538
|
content: FAVICON_BYTES
|
|
1914
2539
|
}
|
|
1915
2540
|
];
|
|
2541
|
+
if (useTypeScript) {
|
|
2542
|
+
files.splice(1, 0, {
|
|
2543
|
+
path: "tsconfig.json",
|
|
2544
|
+
content: JSON.stringify({
|
|
2545
|
+
extends: "./tsconfig.base.json",
|
|
2546
|
+
compilerOptions: {
|
|
2547
|
+
outDir: "./dist",
|
|
2548
|
+
rootDir: "./",
|
|
2549
|
+
types: ["react", "react-dom"]
|
|
2550
|
+
},
|
|
2551
|
+
include: [
|
|
2552
|
+
"rakta-env.d.ts",
|
|
2553
|
+
"app/**/*",
|
|
2554
|
+
"components/**/*",
|
|
2555
|
+
"styles/**/*",
|
|
2556
|
+
"rakta.config.ts"
|
|
2557
|
+
],
|
|
2558
|
+
exclude: ["node_modules", "dist"]
|
|
2559
|
+
}, null, 2)
|
|
2560
|
+
}, {
|
|
2561
|
+
path: "rakta-env.d.ts",
|
|
2562
|
+
content: generateFrontendOnlyRaktaEnv()
|
|
2563
|
+
});
|
|
2564
|
+
}
|
|
2565
|
+
return processFilesForLanguage(files, useTypeScript);
|
|
1916
2566
|
}
|
|
1917
2567
|
function getFullstackFrontendFiles(projectConfig) {
|
|
1918
2568
|
const { projectName, cssFramework } = projectConfig;
|
|
@@ -1934,7 +2584,7 @@ function getFullstackFrontendFiles(projectConfig) {
|
|
|
1934
2584
|
typecheck: "tsc --noEmit"
|
|
1935
2585
|
},
|
|
1936
2586
|
dependencies: {
|
|
1937
|
-
raktajs: "^0.
|
|
2587
|
+
raktajs: "^0.2.0",
|
|
1938
2588
|
react: "^19.2.7",
|
|
1939
2589
|
"react-dom": "^19.2.7",
|
|
1940
2590
|
...getCssDependencies(cssFramework)
|
|
@@ -1974,7 +2624,7 @@ export default defineRaktaConfig({
|
|
|
1974
2624
|
appName: "${projectName}",
|
|
1975
2625
|
seo: {
|
|
1976
2626
|
defaultTitle: "${DEFAULT_METADATA_TITLE}",
|
|
1977
|
-
defaultDescription: "Built with Rakta.js \
|
|
2627
|
+
defaultDescription: "Built with Rakta.js \xE2\u20AC\u201D Small in size. Fierce in speed. Alive in every route.",
|
|
1978
2628
|
},
|
|
1979
2629
|
render: {
|
|
1980
2630
|
defaultMode: "csr",
|
|
@@ -2230,7 +2880,7 @@ export type UserSchema = typeof userSchema;
|
|
|
2230
2880
|
},
|
|
2231
2881
|
{
|
|
2232
2882
|
path: `frontend/styles/${styleFileName}`,
|
|
2233
|
-
content:
|
|
2883
|
+
content: getFrontendOnlyCssGlobals(cssFramework)
|
|
2234
2884
|
},
|
|
2235
2885
|
{
|
|
2236
2886
|
path: "frontend/public/.gitkeep",
|
|
@@ -2250,6 +2900,28 @@ export type UserSchema = typeof userSchema;
|
|
|
2250
2900
|
}
|
|
2251
2901
|
];
|
|
2252
2902
|
}
|
|
2903
|
+
function stripTypeScriptSyntax(code) {
|
|
2904
|
+
return code.replace(/^import\s+type\s+.+;\r?\n/gm, "").replace(/^export\s+interface\s+\w+\s*\{[\s\S]*?\r?\n\}\r?\n/gm, "").replace(/^interface\s+\w+\s*\{[\s\S]*?\r?\n\}\r?\n/gm, "").replace(/^export\s+type\s+\w+\s*=[\s\S]*?;\r?\n/gm, "").replace(/^type\s+\w+\s*=[\s\S]*?;\r?\n/gm, "").replace(/<("[^"]+"|'[^']+'|\w+)(\s*\|\s*("[^"]+"|'[^']+'|\w+))*\s*>/g, "").replace(/\s+as\s+const/g, "").replace(/\s+as\s+[A-Za-z0-9_<>,[\]\s|&".']+/g, "").replace(/\breadonly\s+/g, "").replace(/(\(|,)\s*([A-Za-z_$][\w$]*)\s*:\s*[A-Za-z_$][\w$]*(?:\[\])?(?:\s*\|\s*[A-Za-z_$][\w$]*(?:\[\])?)*/g, "$1 $2").replace(/\)\s*:\s*(?:void|string|number|boolean|Promise<[^>]+>|[A-Za-z_$][\w$]*(?:\[\])?)/g, ")").replace(/\)\s*:\s*[^({=>]+(?=\s*\{)/g, ")").replace(/\}\s*:\s*[A-Za-z_$][\w$]*(?=\s*\))/g, "}").replace(/(const|let)\s+([A-Za-z_$][\w$]*)\s*:\s*[^=;]+=/g, "$1 $2 =");
|
|
2905
|
+
}
|
|
2906
|
+
function processFilesForLanguage(files, useTypeScript) {
|
|
2907
|
+
if (useTypeScript) {
|
|
2908
|
+
return files;
|
|
2909
|
+
}
|
|
2910
|
+
return files.filter((file) => !file.path.endsWith(".d.ts") && !file.path.endsWith("types.ts") && !file.path.endsWith("types.js")).map((file) => {
|
|
2911
|
+
if (typeof file.content !== "string") {
|
|
2912
|
+
return file;
|
|
2913
|
+
}
|
|
2914
|
+
let path = file.path;
|
|
2915
|
+
if (path.endsWith(".tsx"))
|
|
2916
|
+
path = path.replace(/\.tsx$/, ".jsx");
|
|
2917
|
+
else if (path.endsWith(".ts"))
|
|
2918
|
+
path = path.replace(/\.ts$/, ".js");
|
|
2919
|
+
return {
|
|
2920
|
+
path,
|
|
2921
|
+
content: stripTypeScriptSyntax(file.content)
|
|
2922
|
+
};
|
|
2923
|
+
});
|
|
2924
|
+
}
|
|
2253
2925
|
function getBackendFiles(projectConfig) {
|
|
2254
2926
|
return [
|
|
2255
2927
|
...getBackendCommonFiles(projectConfig),
|
|
@@ -2672,430 +3344,238 @@ function getCssDevDependencies(cssFramework) {
|
|
|
2672
3344
|
}
|
|
2673
3345
|
}
|
|
2674
3346
|
function getFrontendOnlyCssGlobals(cssFramework) {
|
|
2675
|
-
const cssImport = cssFramework === "tailwind" ? `@import "
|
|
3347
|
+
const cssImport = cssFramework === "tailwind" ? `@import url("https://fonts.googleapis.com/css2?family=Geist:wght@300;400;500;600;700;800;900&family=JetBrains+Mono:wght@400;500;600;700;800&display=swap");
|
|
3348
|
+
@import "tailwindcss";
|
|
2676
3349
|
|
|
2677
3350
|
` : cssFramework === "bootstrap" ? `@import url("https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css");
|
|
2678
3351
|
|
|
2679
|
-
` : cssFramework === "sass" ? `$color-primary: #
|
|
3352
|
+
` : cssFramework === "sass" ? `$color-primary: #e11d48;
|
|
2680
3353
|
$color-background: #050505;
|
|
2681
|
-
$color-foreground: #
|
|
3354
|
+
$color-foreground: #fafafa;
|
|
2682
3355
|
|
|
2683
3356
|
` : "";
|
|
2684
|
-
return `${cssImport}
|
|
2685
|
-
--
|
|
2686
|
-
--
|
|
2687
|
-
--color-
|
|
2688
|
-
--color-
|
|
2689
|
-
--color-
|
|
2690
|
-
--color-
|
|
2691
|
-
color-
|
|
3357
|
+
return `${cssImport}@theme {
|
|
3358
|
+
--font-sans: "Geist", ui-sans-serif, system-ui, sans-serif;
|
|
3359
|
+
--font-mono: "JetBrains Mono", ui-monospace, SFMono-Regular, monospace;
|
|
3360
|
+
--color-brand-pink: #e11d48;
|
|
3361
|
+
--color-brand-green: #00ff00;
|
|
3362
|
+
--color-surface-bg: #000000;
|
|
3363
|
+
--color-surface-card: #0d0d0d;
|
|
3364
|
+
--color-surface-stroke: #1f1f1f;
|
|
2692
3365
|
}
|
|
2693
3366
|
|
|
2694
|
-
|
|
2695
|
-
|
|
3367
|
+
:root {
|
|
3368
|
+
color-scheme: dark;
|
|
3369
|
+
background: #050505;
|
|
3370
|
+
color: #fafafa;
|
|
3371
|
+
font-family: var(--font-sans);
|
|
2696
3372
|
}
|
|
2697
3373
|
|
|
3374
|
+
* { box-sizing: border-box; }
|
|
3375
|
+
html { scroll-behavior: smooth; }
|
|
2698
3376
|
body {
|
|
2699
3377
|
margin: 0;
|
|
2700
3378
|
min-width: 320px;
|
|
2701
3379
|
min-height: 100vh;
|
|
2702
|
-
|
|
2703
|
-
|
|
2704
|
-
|
|
2705
|
-
ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI",
|
|
2706
|
-
Roboto, sans-serif;
|
|
3380
|
+
background: #050505;
|
|
3381
|
+
color: #fafafa;
|
|
3382
|
+
overflow-x: hidden;
|
|
2707
3383
|
-webkit-font-smoothing: antialiased;
|
|
2708
3384
|
}
|
|
3385
|
+
button, a { font: inherit; }
|
|
3386
|
+
a { color: inherit; text-decoration: none; }
|
|
3387
|
+
#rakta-root { min-height: 100vh; }
|
|
2709
3388
|
|
|
2710
|
-
|
|
2711
|
-
*::before,
|
|
2712
|
-
*::after {
|
|
2713
|
-
box-sizing: border-box;
|
|
2714
|
-
}
|
|
2715
|
-
|
|
2716
|
-
@keyframes shrimpLegs {
|
|
2717
|
-
0% {
|
|
2718
|
-
transform: translateY(0);
|
|
2719
|
-
}
|
|
2720
|
-
|
|
2721
|
-
50% {
|
|
2722
|
-
transform: translateY(2px);
|
|
2723
|
-
}
|
|
2724
|
-
}
|
|
2725
|
-
|
|
2726
|
-
#rakta-root {
|
|
3389
|
+
.rakta-welcome {
|
|
2727
3390
|
min-height: 100vh;
|
|
3391
|
+
background: #050505;
|
|
3392
|
+
color: #fafafa;
|
|
2728
3393
|
}
|
|
2729
|
-
|
|
2730
|
-
|
|
2731
|
-
width: min(1120px, calc(100% - 32px));
|
|
2732
|
-
min-height: 100vh;
|
|
3394
|
+
.rakta-shell {
|
|
3395
|
+
width: min(100% - 32px, 1280px);
|
|
2733
3396
|
margin: 0 auto;
|
|
2734
|
-
padding:
|
|
2735
|
-
}
|
|
2736
|
-
|
|
2737
|
-
section {
|
|
2738
|
-
margin: 0 0 24px;
|
|
2739
|
-
border: 1px solid rgba(248, 113, 113, 0.18);
|
|
2740
|
-
border-radius: 18px;
|
|
2741
|
-
background:
|
|
2742
|
-
linear-gradient(135deg, rgba(220, 38, 38, 0.16), transparent 34%),
|
|
2743
|
-
#0e111a;
|
|
2744
|
-
box-shadow: 0 24px 80px rgba(127, 29, 29, 0.2);
|
|
2745
|
-
padding: clamp(24px, 4vw, 44px);
|
|
3397
|
+
padding: 32px 0;
|
|
2746
3398
|
}
|
|
2747
|
-
|
|
2748
|
-
|
|
2749
|
-
|
|
2750
|
-
|
|
2751
|
-
|
|
2752
|
-
|
|
2753
|
-
|
|
2754
|
-
|
|
2755
|
-
|
|
2756
|
-
|
|
2757
|
-
|
|
2758
|
-
|
|
3399
|
+
.rakta-nav {
|
|
3400
|
+
display: flex;
|
|
3401
|
+
align-items: center;
|
|
3402
|
+
justify-content: space-between;
|
|
3403
|
+
border-bottom: 1px solid #1f1f1f;
|
|
3404
|
+
padding-bottom: 20px;
|
|
3405
|
+
}
|
|
3406
|
+
.rakta-hero {
|
|
3407
|
+
display: grid;
|
|
3408
|
+
min-height: 540px;
|
|
3409
|
+
grid-template-columns: minmax(0, 1.08fr) minmax(320px, 0.92fr);
|
|
3410
|
+
align-items: center;
|
|
3411
|
+
gap: 40px;
|
|
3412
|
+
border-bottom: 1px solid #1f1f1f;
|
|
3413
|
+
padding: 48px 0;
|
|
2759
3414
|
}
|
|
2760
|
-
|
|
2761
|
-
|
|
2762
|
-
|
|
2763
|
-
|
|
3415
|
+
.rakta-hero h1 {
|
|
3416
|
+
max-width: 900px;
|
|
3417
|
+
margin: 0;
|
|
3418
|
+
color: #fff;
|
|
3419
|
+
font-size: clamp(3.5rem, 8vw, 6rem);
|
|
3420
|
+
font-weight: 900;
|
|
3421
|
+
line-height: 0.88;
|
|
2764
3422
|
letter-spacing: 0;
|
|
3423
|
+
text-transform: uppercase;
|
|
2765
3424
|
}
|
|
2766
|
-
|
|
2767
|
-
|
|
2768
|
-
|
|
3425
|
+
.rakta-hero p,
|
|
3426
|
+
.rakta-start p {
|
|
3427
|
+
color: #b5b5b5;
|
|
2769
3428
|
}
|
|
2770
|
-
|
|
2771
|
-
|
|
3429
|
+
.rakta-actions {
|
|
3430
|
+
display: flex;
|
|
3431
|
+
flex-wrap: wrap;
|
|
3432
|
+
gap: 12px;
|
|
3433
|
+
margin-top: 32px;
|
|
3434
|
+
}
|
|
3435
|
+
.rakta-actions a {
|
|
2772
3436
|
display: inline-flex;
|
|
3437
|
+
height: 44px;
|
|
2773
3438
|
align-items: center;
|
|
2774
|
-
|
|
2775
|
-
|
|
2776
|
-
|
|
2777
|
-
|
|
2778
|
-
|
|
2779
|
-
|
|
2780
|
-
|
|
2781
|
-
font-weight: 700;
|
|
2782
|
-
text-decoration: none;
|
|
2783
|
-
cursor: pointer;
|
|
2784
|
-
transition:
|
|
2785
|
-
transform 160ms ease,
|
|
2786
|
-
border-color 160ms ease,
|
|
2787
|
-
background 160ms ease;
|
|
3439
|
+
gap: 8px;
|
|
3440
|
+
border: 1px solid #e11d48;
|
|
3441
|
+
padding: 0 20px;
|
|
3442
|
+
font-family: var(--font-mono);
|
|
3443
|
+
font-size: 12px;
|
|
3444
|
+
font-weight: 800;
|
|
3445
|
+
text-transform: uppercase;
|
|
2788
3446
|
}
|
|
2789
|
-
|
|
2790
|
-
|
|
2791
|
-
|
|
2792
|
-
background: rgba(220, 38, 38, 0.12);
|
|
2793
|
-
transform: translateY(-1px);
|
|
3447
|
+
.rakta-actions a:first-child {
|
|
3448
|
+
background: #e11d48;
|
|
3449
|
+
color: #fff;
|
|
2794
3450
|
}
|
|
2795
|
-
|
|
2796
|
-
|
|
2797
|
-
|
|
3451
|
+
.rakta-status-grid,
|
|
3452
|
+
.rakta-feature-grid {
|
|
3453
|
+
display: grid;
|
|
3454
|
+
gap: 12px;
|
|
3455
|
+
border: 1px solid #1f1f1f;
|
|
3456
|
+
background: #0d0d0d;
|
|
3457
|
+
padding: 20px;
|
|
3458
|
+
}
|
|
3459
|
+
.rakta-status-grid > div,
|
|
3460
|
+
.rakta-feature-grid > div {
|
|
3461
|
+
border: 1px solid rgba(255, 255, 255, 0.06);
|
|
3462
|
+
background: #000;
|
|
3463
|
+
padding: 16px;
|
|
3464
|
+
}
|
|
3465
|
+
.rakta-feature-grid {
|
|
3466
|
+
grid-template-columns: repeat(3, minmax(0, 1fr));
|
|
3467
|
+
gap: 0;
|
|
3468
|
+
padding: 0;
|
|
3469
|
+
}
|
|
3470
|
+
.rakta-feature-grid > div {
|
|
3471
|
+
min-height: 220px;
|
|
3472
|
+
border-color: #1f1f1f;
|
|
3473
|
+
padding: 28px;
|
|
3474
|
+
}
|
|
3475
|
+
.rakta-game-section {
|
|
3476
|
+
display: grid;
|
|
3477
|
+
gap: 20px;
|
|
3478
|
+
}
|
|
3479
|
+
.rakta-game-section h2,
|
|
3480
|
+
.rakta-start h2 {
|
|
3481
|
+
margin: 12px 0 0;
|
|
3482
|
+
font-size: clamp(2.25rem, 6vw, 4rem);
|
|
3483
|
+
font-weight: 900;
|
|
3484
|
+
line-height: 0.95;
|
|
3485
|
+
text-transform: uppercase;
|
|
2798
3486
|
}
|
|
2799
|
-
|
|
2800
|
-
.relative {
|
|
3487
|
+
.rakta-game-field {
|
|
2801
3488
|
position: relative;
|
|
2802
|
-
}
|
|
2803
|
-
|
|
2804
|
-
.absolute {
|
|
2805
|
-
position: absolute;
|
|
2806
|
-
}
|
|
2807
|
-
|
|
2808
|
-
.inset-0 {
|
|
2809
|
-
inset: 0;
|
|
2810
|
-
}
|
|
2811
|
-
|
|
2812
|
-
.bottom-0 {
|
|
2813
|
-
bottom: 0;
|
|
2814
|
-
}
|
|
2815
|
-
|
|
2816
|
-
.left-0 {
|
|
2817
|
-
left: 0;
|
|
2818
|
-
}
|
|
2819
|
-
|
|
2820
|
-
.top-2 {
|
|
2821
|
-
top: 8px;
|
|
2822
|
-
}
|
|
2823
|
-
|
|
2824
|
-
.right-3 {
|
|
2825
|
-
right: 12px;
|
|
2826
|
-
}
|
|
2827
|
-
|
|
2828
|
-
.block {
|
|
2829
3489
|
display: block;
|
|
3490
|
+
width: 100%;
|
|
3491
|
+
height: 280px;
|
|
3492
|
+
overflow: hidden;
|
|
3493
|
+
border: 2px solid #1f1f1f;
|
|
3494
|
+
background: #000;
|
|
3495
|
+
color: inherit;
|
|
3496
|
+
cursor: pointer;
|
|
2830
3497
|
}
|
|
2831
|
-
|
|
2832
|
-
.flex {
|
|
3498
|
+
.rakta-game-controls {
|
|
2833
3499
|
display: flex;
|
|
2834
|
-
}
|
|
2835
|
-
|
|
2836
|
-
.flex-col {
|
|
2837
|
-
flex-direction: column;
|
|
2838
|
-
}
|
|
2839
|
-
|
|
2840
|
-
.flex-wrap {
|
|
2841
3500
|
flex-wrap: wrap;
|
|
2842
|
-
}
|
|
2843
|
-
|
|
2844
|
-
.items-center {
|
|
2845
3501
|
align-items: center;
|
|
2846
|
-
|
|
2847
|
-
|
|
2848
|
-
.items-start {
|
|
2849
|
-
align-items: flex-start;
|
|
2850
|
-
}
|
|
2851
|
-
|
|
2852
|
-
.justify-center {
|
|
2853
|
-
justify-content: center;
|
|
2854
|
-
}
|
|
2855
|
-
|
|
2856
|
-
.gap-2 {
|
|
2857
|
-
gap: 8px;
|
|
2858
|
-
}
|
|
2859
|
-
|
|
2860
|
-
.gap-4 {
|
|
3502
|
+
justify-content: space-between;
|
|
2861
3503
|
gap: 16px;
|
|
2862
|
-
|
|
2863
|
-
|
|
2864
|
-
|
|
2865
|
-
|
|
2866
|
-
|
|
2867
|
-
|
|
2868
|
-
|
|
2869
|
-
|
|
2870
|
-
}
|
|
2871
|
-
|
|
2872
|
-
.max-w-full {
|
|
2873
|
-
max-width: 100%;
|
|
2874
|
-
}
|
|
2875
|
-
|
|
2876
|
-
.overflow-hidden {
|
|
2877
|
-
overflow: hidden;
|
|
2878
|
-
}
|
|
2879
|
-
|
|
2880
|
-
.select-none {
|
|
2881
|
-
user-select: none;
|
|
2882
|
-
}
|
|
2883
|
-
|
|
2884
|
-
.cursor-pointer {
|
|
2885
|
-
cursor: pointer;
|
|
2886
|
-
}
|
|
2887
|
-
|
|
2888
|
-
.text-left {
|
|
2889
|
-
text-align: left;
|
|
2890
|
-
}
|
|
2891
|
-
|
|
2892
|
-
.text-sm {
|
|
2893
|
-
font-size: 14px;
|
|
2894
|
-
}
|
|
2895
|
-
|
|
2896
|
-
.text-lg {
|
|
2897
|
-
font-size: 18px;
|
|
2898
|
-
}
|
|
2899
|
-
|
|
2900
|
-
.text-xl {
|
|
2901
|
-
font-size: 20px;
|
|
2902
|
-
}
|
|
2903
|
-
|
|
2904
|
-
.font-bold {
|
|
2905
|
-
font-weight: 800;
|
|
2906
|
-
}
|
|
2907
|
-
|
|
2908
|
-
.font-semibold {
|
|
2909
|
-
font-weight: 700;
|
|
2910
|
-
}
|
|
2911
|
-
|
|
2912
|
-
.font-mono {
|
|
2913
|
-
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
|
|
2914
|
-
}
|
|
2915
|
-
|
|
2916
|
-
.text-white {
|
|
2917
|
-
color: #fff;
|
|
2918
|
-
}
|
|
2919
|
-
|
|
2920
|
-
.text-slate-400 {
|
|
2921
|
-
color: #94a3b8;
|
|
2922
|
-
}
|
|
2923
|
-
|
|
2924
|
-
.text-red-600 {
|
|
2925
|
-
color: #dc2626;
|
|
2926
|
-
}
|
|
2927
|
-
|
|
2928
|
-
.bg-red-600 {
|
|
2929
|
-
background: #dc2626;
|
|
2930
|
-
}
|
|
2931
|
-
|
|
2932
|
-
.bg-black\\/75 {
|
|
2933
|
-
background: rgba(0, 0, 0, 0.75);
|
|
2934
|
-
}
|
|
2935
|
-
|
|
2936
|
-
.pointer-events-none {
|
|
2937
|
-
pointer-events: none;
|
|
2938
|
-
}
|
|
2939
|
-
|
|
2940
|
-
.rounded-sm {
|
|
2941
|
-
border-radius: 2px;
|
|
2942
|
-
}
|
|
2943
|
-
|
|
2944
|
-
.rounded-lg {
|
|
2945
|
-
border-radius: 8px;
|
|
2946
|
-
}
|
|
2947
|
-
|
|
2948
|
-
.min-h-5 {
|
|
2949
|
-
min-height: 20px;
|
|
2950
|
-
}
|
|
2951
|
-
|
|
2952
|
-
.tabular-nums {
|
|
2953
|
-
font-variant-numeric: tabular-nums;
|
|
2954
|
-
}
|
|
2955
|
-
|
|
2956
|
-
button[aria-label^="ShrimpRun"] {
|
|
2957
|
-
border: 2px solid rgba(248, 113, 113, 0.28);
|
|
2958
|
-
border-radius: 16px;
|
|
2959
|
-
background:
|
|
2960
|
-
linear-gradient(180deg, rgba(15, 23, 42, 0.96), rgba(7, 10, 18, 0.98)),
|
|
2961
|
-
radial-gradient(circle at 18% 22%, rgba(248, 113, 113, 0.16), transparent 30%);
|
|
2962
|
-
box-shadow:
|
|
2963
|
-
inset 0 0 0 1px rgba(255, 255, 255, 0.04),
|
|
2964
|
-
0 20px 60px rgba(0, 0, 0, 0.28);
|
|
2965
|
-
}
|
|
2966
|
-
|
|
2967
|
-
button[aria-label^="ShrimpRun"] > .bottom-0.left-0 {
|
|
2968
|
-
background: linear-gradient(90deg, #7f1d1d, #dc2626, #fecaca);
|
|
2969
|
-
}
|
|
2970
|
-
|
|
2971
|
-
button:not([aria-label]) {
|
|
2972
|
-
border: 0;
|
|
2973
|
-
border-radius: 10px;
|
|
2974
|
-
background: #dc2626;
|
|
2975
|
-
color: #fff;
|
|
3504
|
+
font-family: var(--font-mono);
|
|
3505
|
+
font-size: 10px;
|
|
3506
|
+
color: #71717a;
|
|
3507
|
+
}
|
|
3508
|
+
.rakta-game-controls button {
|
|
3509
|
+
border: 1px solid #1f1f1f;
|
|
3510
|
+
padding: 4px 8px;
|
|
3511
|
+
color: #a1a1aa;
|
|
2976
3512
|
cursor: pointer;
|
|
2977
|
-
font-weight: 800;
|
|
2978
|
-
}
|
|
2979
|
-
|
|
2980
|
-
.shrimp-sprite {
|
|
2981
|
-
filter: drop-shadow(0 8px 12px rgba(0, 0, 0, 0.32));
|
|
2982
3513
|
}
|
|
2983
|
-
|
|
2984
|
-
|
|
2985
|
-
|
|
2986
|
-
background:
|
|
2987
|
-
radial-gradient(circle at 50% 8px, #fecaca 0 4px, transparent 5px),
|
|
2988
|
-
linear-gradient(135deg, #fb7185 0%, #dc2626 46%, #7f1d1d 100%);
|
|
2989
|
-
box-shadow:
|
|
2990
|
-
inset -6px -8px 0 rgba(69, 10, 10, 0.32),
|
|
2991
|
-
0 0 0 2px rgba(254, 202, 202, 0.18);
|
|
2992
|
-
}
|
|
2993
|
-
|
|
2994
|
-
.shrimp-run-obstacle::before,
|
|
2995
|
-
.shrimp-run-obstacle::after {
|
|
2996
|
-
position: absolute;
|
|
2997
|
-
bottom: 8px;
|
|
2998
|
-
width: 10px;
|
|
2999
|
-
height: 22px;
|
|
3000
|
-
border-radius: 999px 999px 4px 4px;
|
|
3001
|
-
background: #ef4444;
|
|
3002
|
-
content: "";
|
|
3514
|
+
.rakta-start {
|
|
3515
|
+
border-top: 1px solid #1f1f1f;
|
|
3516
|
+
padding: 40px 0;
|
|
3003
3517
|
}
|
|
3004
3518
|
|
|
3005
|
-
|
|
3006
|
-
|
|
3007
|
-
|
|
3008
|
-
|
|
3009
|
-
|
|
3010
|
-
.shrimp-run-obstacle::after {
|
|
3011
|
-
right: -7px;
|
|
3012
|
-
transform: rotate(18deg);
|
|
3013
|
-
}
|
|
3014
|
-
|
|
3015
|
-
@media (max-width: 720px) {
|
|
3016
|
-
main {
|
|
3017
|
-
width: min(100% - 20px, 1120px);
|
|
3018
|
-
padding: 24px 0;
|
|
3519
|
+
@media (max-width: 768px) {
|
|
3520
|
+
.rakta-hero,
|
|
3521
|
+
.rakta-feature-grid {
|
|
3522
|
+
grid-template-columns: 1fr;
|
|
3019
3523
|
}
|
|
3020
|
-
|
|
3021
|
-
|
|
3022
|
-
padding: 20px;
|
|
3524
|
+
.rakta-nav > div:last-child {
|
|
3525
|
+
display: none;
|
|
3023
3526
|
}
|
|
3024
3527
|
}
|
|
3025
|
-
`;
|
|
3026
|
-
}
|
|
3027
|
-
function getCssGlobals(cssFramework) {
|
|
3028
|
-
const sharedBase = `
|
|
3029
|
-
:root {
|
|
3030
|
-
--color-primary: #dc2626;
|
|
3031
|
-
--color-background: #050505;
|
|
3032
|
-
--color-foreground: #f8fafc;
|
|
3033
|
-
}
|
|
3034
3528
|
|
|
3035
|
-
|
|
3036
|
-
|
|
3037
|
-
}
|
|
3529
|
+
::-webkit-scrollbar { width: 6px; height: 6px; }
|
|
3530
|
+
::-webkit-scrollbar-track { background: #000000; }
|
|
3531
|
+
::-webkit-scrollbar-thumb { background: #e11d48; border-radius: 0; }
|
|
3532
|
+
::-webkit-scrollbar-thumb:hover { background: #be123c; }
|
|
3038
3533
|
|
|
3039
|
-
|
|
3040
|
-
|
|
3041
|
-
|
|
3042
|
-
background: var(--color-background);
|
|
3043
|
-
font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
|
3534
|
+
@keyframes scanline {
|
|
3535
|
+
0% { transform: translateY(-100%); }
|
|
3536
|
+
100% { transform: translateY(100vh); }
|
|
3044
3537
|
}
|
|
3045
|
-
|
|
3046
|
-
|
|
3047
|
-
|
|
3048
|
-
|
|
3049
|
-
|
|
3538
|
+
.scanline {
|
|
3539
|
+
position: fixed;
|
|
3540
|
+
top: 0;
|
|
3541
|
+
left: 0;
|
|
3542
|
+
z-index: 999;
|
|
3543
|
+
width: 100%;
|
|
3544
|
+
height: 120px;
|
|
3545
|
+
pointer-events: none;
|
|
3546
|
+
background: linear-gradient(0deg, rgba(225, 29, 72, 0.08) 0%, rgba(225, 29, 72, 0) 100%);
|
|
3547
|
+
opacity: 0.8;
|
|
3548
|
+
animation: scanline 8s linear infinite;
|
|
3050
3549
|
}
|
|
3051
3550
|
|
|
3052
|
-
.
|
|
3053
|
-
|
|
3054
|
-
|
|
3055
|
-
|
|
3056
|
-
|
|
3057
|
-
padding: 2rem;
|
|
3058
|
-
margin-bottom: 1.5rem;
|
|
3551
|
+
.bg-grid-glow {
|
|
3552
|
+
background-size: 40px 40px;
|
|
3553
|
+
background-image:
|
|
3554
|
+
linear-gradient(to right, rgba(255, 255, 255, 0.04) 1px, transparent 1px),
|
|
3555
|
+
linear-gradient(to bottom, rgba(255, 255, 255, 0.04) 1px, transparent 1px);
|
|
3059
3556
|
}
|
|
3060
3557
|
|
|
3061
|
-
|
|
3062
|
-
|
|
3063
|
-
|
|
3064
|
-
|
|
3065
|
-
font-size: 0.75rem;
|
|
3066
|
-
text-transform: uppercase;
|
|
3558
|
+
@keyframes seaweed-wave-1 {
|
|
3559
|
+
0% { transform: skewX(-14deg) rotate(-8deg) scaleY(0.96); }
|
|
3560
|
+
50% { transform: skewX(0deg) rotate(0deg) scaleY(1.04); }
|
|
3561
|
+
100% { transform: skewX(14deg) rotate(8deg) scaleY(0.96); }
|
|
3067
3562
|
}
|
|
3068
|
-
|
|
3069
|
-
.
|
|
3070
|
-
|
|
3071
|
-
|
|
3072
|
-
flex-wrap: wrap;
|
|
3073
|
-
margin-top: 1rem;
|
|
3563
|
+
@keyframes seaweed-wave-2 {
|
|
3564
|
+
0% { transform: skewX(10deg) rotate(6deg) scaleY(1.04); }
|
|
3565
|
+
50% { transform: skewX(-2deg) rotate(-2deg) scaleY(0.96); }
|
|
3566
|
+
100% { transform: skewX(-10deg) rotate(-6deg) scaleY(1.04); }
|
|
3074
3567
|
}
|
|
3075
|
-
|
|
3076
|
-
|
|
3077
|
-
|
|
3568
|
+
.seaweed-waving-left-1,
|
|
3569
|
+
.seaweed-waving-right-1 {
|
|
3570
|
+
transform-origin: bottom center !important;
|
|
3571
|
+
animation: seaweed-wave-1 3.2s infinite ease-in-out alternate !important;
|
|
3078
3572
|
}
|
|
3079
|
-
|
|
3080
|
-
|
|
3081
|
-
|
|
3573
|
+
.seaweed-waving-left-2,
|
|
3574
|
+
.seaweed-waving-right-2 {
|
|
3575
|
+
transform-origin: bottom center !important;
|
|
3576
|
+
animation: seaweed-wave-2 3.8s infinite ease-in-out alternate !important;
|
|
3082
3577
|
}
|
|
3083
3578
|
`;
|
|
3084
|
-
switch (cssFramework) {
|
|
3085
|
-
case "tailwind":
|
|
3086
|
-
return `@import "tailwindcss";
|
|
3087
|
-
${sharedBase}`;
|
|
3088
|
-
case "bootstrap":
|
|
3089
|
-
return `@import url("https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css");
|
|
3090
|
-
${sharedBase}`;
|
|
3091
|
-
case "sass":
|
|
3092
|
-
return `$color-primary: #dc2626;
|
|
3093
|
-
$color-background: #050505;
|
|
3094
|
-
$color-foreground: #f8fafc;
|
|
3095
|
-
${sharedBase}`;
|
|
3096
|
-
case "none":
|
|
3097
|
-
return sharedBase;
|
|
3098
|
-
}
|
|
3099
3579
|
}
|
|
3100
3580
|
function generateFrontendOnlyRaktaEnv() {
|
|
3101
3581
|
return `declare module "*.css";
|
|
@@ -3115,14 +3595,6 @@ declare global {
|
|
|
3115
3595
|
click: RaktaClickAttributes;
|
|
3116
3596
|
}
|
|
3117
3597
|
}
|
|
3118
|
-
|
|
3119
|
-
const useCallback: typeof import("react").useCallback;
|
|
3120
|
-
const useEffect: typeof import("react").useEffect;
|
|
3121
|
-
const useRef: typeof import("react").useRef;
|
|
3122
|
-
const useState: typeof import("react").useState;
|
|
3123
|
-
|
|
3124
|
-
const ShrimpRunGame: typeof import("./app/components/shrimpRunGame").default;
|
|
3125
|
-
const RaktaShrimpMascot: typeof import("./app/components/raktaShrimpMascot").default;
|
|
3126
3598
|
}
|
|
3127
3599
|
|
|
3128
3600
|
declare module "react/jsx-runtime" {
|
|
@@ -3165,54 +3637,8 @@ export default function RootLayout({ children }: RootLayoutProps) {
|
|
|
3165
3637
|
}
|
|
3166
3638
|
`;
|
|
3167
3639
|
}
|
|
3168
|
-
function generateFrontendOnlyPage(
|
|
3169
|
-
return
|
|
3170
|
-
|
|
3171
|
-
export default function HomePage() {
|
|
3172
|
-
return (
|
|
3173
|
-
<main className="mx-auto flex min-h-screen w-full max-w-4xl flex-col gap-6 px-4 py-16 sm:px-6 lg:px-8">
|
|
3174
|
-
<section className="rounded-3xl border border-white/10 bg-[#0e111a] p-8 shadow-2xl shadow-red-950/20">
|
|
3175
|
-
<p className="mb-2 text-xs font-bold uppercase tracking-[0.3em] text-red-600">
|
|
3176
|
-
THE RED ROUTER FRAMEWORK
|
|
3177
|
-
</p>
|
|
3178
|
-
<h1 className="mb-3 text-4xl font-extrabold leading-tight text-white md:text-6xl">
|
|
3179
|
-
Welcome to ${projectName}
|
|
3180
|
-
</h1>
|
|
3181
|
-
<p className="max-w-2xl text-base leading-7 text-slate-400">
|
|
3182
|
-
Built with Rakta.js \u2014 Small in size. Fierce in speed. Alive in every route.
|
|
3183
|
-
</p>
|
|
3184
|
-
|
|
3185
|
-
<div className="mt-6 flex flex-wrap gap-3">
|
|
3186
|
-
<click
|
|
3187
|
-
to="/offline"
|
|
3188
|
-
className="rounded-xl border border-red-500/40 px-4 py-2 text-sm font-semibold text-red-400 transition hover:border-red-400 hover:text-red-300"
|
|
3189
|
-
>
|
|
3190
|
-
Open offline page
|
|
3191
|
-
</click>
|
|
3192
|
-
<click
|
|
3193
|
-
to="/dashboard"
|
|
3194
|
-
className="rounded-xl bg-red-600 px-4 py-2 text-sm font-semibold text-white transition hover:bg-red-700"
|
|
3195
|
-
>
|
|
3196
|
-
View dashboard
|
|
3197
|
-
</click>
|
|
3198
|
-
</div>
|
|
3199
|
-
</section>
|
|
3200
|
-
|
|
3201
|
-
<section className="rounded-3xl border border-white/10 bg-[#0e111a] p-8 shadow-2xl shadow-red-950/20">
|
|
3202
|
-
<p className="mb-2 text-xs font-bold uppercase tracking-[0.3em] text-red-600">
|
|
3203
|
-
SHRIMPRUN
|
|
3204
|
-
</p>
|
|
3205
|
-
<h2 className="mb-1 text-2xl font-bold text-white">Play ShrimpRun</h2>
|
|
3206
|
-
<p className="mb-6 text-sm leading-6 text-slate-400">
|
|
3207
|
-
Press Space or click the game area to jump. Avoid the red obstacles!
|
|
3208
|
-
</p>
|
|
3209
|
-
|
|
3210
|
-
<ShrimpRunGame />
|
|
3211
|
-
</section>
|
|
3212
|
-
</main>
|
|
3213
|
-
);
|
|
3214
|
-
}
|
|
3215
|
-
`;
|
|
3640
|
+
function generateFrontendOnlyPage(_projectName) {
|
|
3641
|
+
return STARTER_PAGE_CODE;
|
|
3216
3642
|
}
|
|
3217
3643
|
function generateFrontendOnlyLoading() {
|
|
3218
3644
|
return `export default function Loading() {
|
|
@@ -3282,564 +3708,6 @@ function generateFrontendOnlyNotFound() {
|
|
|
3282
3708
|
}
|
|
3283
3709
|
`;
|
|
3284
3710
|
}
|
|
3285
|
-
function generateShrimpMascotComponent() {
|
|
3286
|
-
return `interface RaktaShrimpMascotProps {
|
|
3287
|
-
readonly isJumping: boolean;
|
|
3288
|
-
readonly isDead: boolean;
|
|
3289
|
-
readonly style?: import("react").CSSProperties;
|
|
3290
|
-
}
|
|
3291
|
-
|
|
3292
|
-
/**
|
|
3293
|
-
* RaktaShrimpMascot \u2014 The animated shrimp hero of ShrimpRun.
|
|
3294
|
-
* Drawn entirely with inline SVG. No external assets required.
|
|
3295
|
-
*/
|
|
3296
|
-
export default function RaktaShrimpMascot({
|
|
3297
|
-
isJumping,
|
|
3298
|
-
isDead,
|
|
3299
|
-
style,
|
|
3300
|
-
}: RaktaShrimpMascotProps) {
|
|
3301
|
-
const bodyColor = isDead ? "#6b7280" : "#dc2626";
|
|
3302
|
-
const eyeColor = isDead ? "#374151" : "#fff";
|
|
3303
|
-
const legAnimation =
|
|
3304
|
-
isJumping || isDead ? "none" : "shrimpLegs 0.3s steps(2) infinite";
|
|
3305
|
-
|
|
3306
|
-
return (
|
|
3307
|
-
<svg
|
|
3308
|
-
viewBox="0 0 48 48"
|
|
3309
|
-
width="48"
|
|
3310
|
-
height="48"
|
|
3311
|
-
style={{ display: "block", ...style }}
|
|
3312
|
-
aria-label={
|
|
3313
|
-
isDead ? "dead shrimp" : isJumping ? "shrimp jumping" : "running shrimp"
|
|
3314
|
-
}
|
|
3315
|
-
role="img"
|
|
3316
|
-
>
|
|
3317
|
-
<style>{\`
|
|
3318
|
-
@keyframes shrimpLegs {
|
|
3319
|
-
0% { transform: translateY(0); }
|
|
3320
|
-
50% { transform: translateY(2px); }
|
|
3321
|
-
}
|
|
3322
|
-
\`}</style>
|
|
3323
|
-
|
|
3324
|
-
{/* Body */}
|
|
3325
|
-
<path
|
|
3326
|
-
d="M8 30 Q10 14 24 12 Q38 10 40 22 Q42 32 32 36 Q20 40 8 30Z"
|
|
3327
|
-
fill={bodyColor}
|
|
3328
|
-
/>
|
|
3329
|
-
|
|
3330
|
-
{/* Shell segments */}
|
|
3331
|
-
<path
|
|
3332
|
-
d="M12 28 Q16 20 24 18 Q30 17 34 22"
|
|
3333
|
-
stroke="#b91c1c"
|
|
3334
|
-
strokeWidth="1.5"
|
|
3335
|
-
fill="none"
|
|
3336
|
-
opacity={isDead ? 0.3 : 0.6}
|
|
3337
|
-
/>
|
|
3338
|
-
<path
|
|
3339
|
-
d="M14 32 Q19 24 28 22 Q33 21 36 26"
|
|
3340
|
-
stroke="#b91c1c"
|
|
3341
|
-
strokeWidth="1.5"
|
|
3342
|
-
fill="none"
|
|
3343
|
-
opacity={isDead ? 0.3 : 0.6}
|
|
3344
|
-
/>
|
|
3345
|
-
|
|
3346
|
-
{/* Eye */}
|
|
3347
|
-
<circle cx="34" cy="18" r="4" fill="#1e293b" />
|
|
3348
|
-
<circle cx="35" cy="17" r="2" fill={eyeColor} />
|
|
3349
|
-
|
|
3350
|
-
{isDead && (
|
|
3351
|
-
<>
|
|
3352
|
-
<line
|
|
3353
|
-
x1="32"
|
|
3354
|
-
y1="16"
|
|
3355
|
-
x2="36"
|
|
3356
|
-
y2="20"
|
|
3357
|
-
stroke="#374151"
|
|
3358
|
-
strokeWidth="1.5"
|
|
3359
|
-
/>
|
|
3360
|
-
<line
|
|
3361
|
-
x1="36"
|
|
3362
|
-
y1="16"
|
|
3363
|
-
x2="32"
|
|
3364
|
-
y2="20"
|
|
3365
|
-
stroke="#374151"
|
|
3366
|
-
strokeWidth="1.5"
|
|
3367
|
-
/>
|
|
3368
|
-
</>
|
|
3369
|
-
)}
|
|
3370
|
-
|
|
3371
|
-
{/* Antennae */}
|
|
3372
|
-
<line
|
|
3373
|
-
x1="34"
|
|
3374
|
-
y1="14"
|
|
3375
|
-
x2="40"
|
|
3376
|
-
y2="6"
|
|
3377
|
-
stroke={bodyColor}
|
|
3378
|
-
strokeWidth="1.5"
|
|
3379
|
-
strokeLinecap="round"
|
|
3380
|
-
/>
|
|
3381
|
-
<line
|
|
3382
|
-
x1="32"
|
|
3383
|
-
y1="13"
|
|
3384
|
-
x2="36"
|
|
3385
|
-
y2="4"
|
|
3386
|
-
stroke={bodyColor}
|
|
3387
|
-
strokeWidth="1.5"
|
|
3388
|
-
strokeLinecap="round"
|
|
3389
|
-
/>
|
|
3390
|
-
|
|
3391
|
-
{/* Tail */}
|
|
3392
|
-
<path
|
|
3393
|
-
d="M10 30 Q4 26 6 20"
|
|
3394
|
-
stroke={bodyColor}
|
|
3395
|
-
strokeWidth="2"
|
|
3396
|
-
fill="none"
|
|
3397
|
-
strokeLinecap="round"
|
|
3398
|
-
/>
|
|
3399
|
-
<path
|
|
3400
|
-
d="M10 30 Q2 30 4 36"
|
|
3401
|
-
stroke={bodyColor}
|
|
3402
|
-
strokeWidth="2"
|
|
3403
|
-
fill="none"
|
|
3404
|
-
strokeLinecap="round"
|
|
3405
|
-
/>
|
|
3406
|
-
<path
|
|
3407
|
-
d="M10 30 Q6 34 8 40"
|
|
3408
|
-
stroke={bodyColor}
|
|
3409
|
-
strokeWidth="2"
|
|
3410
|
-
fill="none"
|
|
3411
|
-
strokeLinecap="round"
|
|
3412
|
-
/>
|
|
3413
|
-
|
|
3414
|
-
{/* Legs */}
|
|
3415
|
-
<g
|
|
3416
|
-
style={{
|
|
3417
|
-
animation: legAnimation,
|
|
3418
|
-
transformOrigin: "24px 36px",
|
|
3419
|
-
}}
|
|
3420
|
-
>
|
|
3421
|
-
<line
|
|
3422
|
-
x1="18"
|
|
3423
|
-
y1="36"
|
|
3424
|
-
x2="14"
|
|
3425
|
-
y2="44"
|
|
3426
|
-
stroke={bodyColor}
|
|
3427
|
-
strokeWidth="1.5"
|
|
3428
|
-
strokeLinecap="round"
|
|
3429
|
-
/>
|
|
3430
|
-
<line
|
|
3431
|
-
x1="22"
|
|
3432
|
-
y1="38"
|
|
3433
|
-
x2="18"
|
|
3434
|
-
y2="46"
|
|
3435
|
-
stroke={bodyColor}
|
|
3436
|
-
strokeWidth="1.5"
|
|
3437
|
-
strokeLinecap="round"
|
|
3438
|
-
/>
|
|
3439
|
-
<line
|
|
3440
|
-
x1="26"
|
|
3441
|
-
y1="38"
|
|
3442
|
-
x2="24"
|
|
3443
|
-
y2="46"
|
|
3444
|
-
stroke={bodyColor}
|
|
3445
|
-
strokeWidth="1.5"
|
|
3446
|
-
strokeLinecap="round"
|
|
3447
|
-
/>
|
|
3448
|
-
<line
|
|
3449
|
-
x1="30"
|
|
3450
|
-
y1="37"
|
|
3451
|
-
x2="28"
|
|
3452
|
-
y2="45"
|
|
3453
|
-
stroke={bodyColor}
|
|
3454
|
-
strokeWidth="1.5"
|
|
3455
|
-
strokeLinecap="round"
|
|
3456
|
-
/>
|
|
3457
|
-
</g>
|
|
3458
|
-
</svg>
|
|
3459
|
-
);
|
|
3460
|
-
}
|
|
3461
|
-
`;
|
|
3462
|
-
}
|
|
3463
|
-
function generateShrimpRunGameComponent() {
|
|
3464
|
-
return `import { useCallback, useEffect, useRef, useState } from "react";
|
|
3465
|
-
import RaktaShrimpMascot from "./raktaShrimpMascot";
|
|
3466
|
-
// \u2500\u2500\u2500 Types \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
3467
|
-
|
|
3468
|
-
type GameStatus = "idle" | "running" | "dead";
|
|
3469
|
-
|
|
3470
|
-
interface ObstacleState {
|
|
3471
|
-
readonly id: number;
|
|
3472
|
-
readonly xPosition: number;
|
|
3473
|
-
readonly width: number;
|
|
3474
|
-
readonly height: number;
|
|
3475
|
-
}
|
|
3476
|
-
|
|
3477
|
-
interface ShrimpState {
|
|
3478
|
-
readonly yPosition: number;
|
|
3479
|
-
readonly velocityY: number;
|
|
3480
|
-
readonly isJumping: boolean;
|
|
3481
|
-
}
|
|
3482
|
-
|
|
3483
|
-
// \u2500\u2500\u2500 Constants \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
3484
|
-
|
|
3485
|
-
const CANVAS_WIDTH = 640;
|
|
3486
|
-
const CANVAS_HEIGHT = 160;
|
|
3487
|
-
const GROUND_STRIP_HEIGHT = 4;
|
|
3488
|
-
const SHRIMP_START_X = 60;
|
|
3489
|
-
const SHRIMP_WIDTH = 48;
|
|
3490
|
-
const SHRIMP_HEIGHT = 48;
|
|
3491
|
-
const GRAVITY = 1.4;
|
|
3492
|
-
const JUMP_VELOCITY = -18;
|
|
3493
|
-
const INITIAL_OBSTACLE_SPEED = 5;
|
|
3494
|
-
const SPEED_INCREMENT_PER_SCORE = 0.003;
|
|
3495
|
-
const OBSTACLE_SPAWN_INTERVAL_MS = 1600;
|
|
3496
|
-
const SCORE_TICK_MS = 80;
|
|
3497
|
-
const COLLISION_MARGIN = 8;
|
|
3498
|
-
const MAX_CONCURRENT_OBSTACLES = 3;
|
|
3499
|
-
const FRAME_SKIP_THRESHOLD_MS = 100;
|
|
3500
|
-
|
|
3501
|
-
// \u2500\u2500\u2500 Physics helpers \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
3502
|
-
|
|
3503
|
-
function getObstacleSpeed(currentScore: number): number {
|
|
3504
|
-
return INITIAL_OBSTACLE_SPEED + currentScore * SPEED_INCREMENT_PER_SCORE;
|
|
3505
|
-
}
|
|
3506
|
-
|
|
3507
|
-
function checkCollision(
|
|
3508
|
-
shrimpYPosition: number,
|
|
3509
|
-
obstacle: ObstacleState,
|
|
3510
|
-
): boolean {
|
|
3511
|
-
const shrimpLeft = SHRIMP_START_X + COLLISION_MARGIN;
|
|
3512
|
-
const shrimpRight = SHRIMP_START_X + SHRIMP_WIDTH - COLLISION_MARGIN;
|
|
3513
|
-
const shrimpTop =
|
|
3514
|
-
CANVAS_HEIGHT -
|
|
3515
|
-
GROUND_STRIP_HEIGHT -
|
|
3516
|
-
shrimpYPosition -
|
|
3517
|
-
SHRIMP_HEIGHT +
|
|
3518
|
-
COLLISION_MARGIN;
|
|
3519
|
-
const shrimpBottom = CANVAS_HEIGHT - GROUND_STRIP_HEIGHT - shrimpYPosition;
|
|
3520
|
-
|
|
3521
|
-
const obstacleLeft = obstacle.xPosition + COLLISION_MARGIN;
|
|
3522
|
-
const obstacleRight = obstacle.xPosition + obstacle.width - COLLISION_MARGIN;
|
|
3523
|
-
const obstacleTop = CANVAS_HEIGHT - GROUND_STRIP_HEIGHT - obstacle.height;
|
|
3524
|
-
const obstacleBottom = CANVAS_HEIGHT - GROUND_STRIP_HEIGHT;
|
|
3525
|
-
|
|
3526
|
-
return (
|
|
3527
|
-
shrimpLeft < obstacleRight &&
|
|
3528
|
-
shrimpRight > obstacleLeft &&
|
|
3529
|
-
shrimpTop < obstacleBottom &&
|
|
3530
|
-
shrimpBottom > obstacleTop
|
|
3531
|
-
);
|
|
3532
|
-
}
|
|
3533
|
-
|
|
3534
|
-
// \u2500\u2500\u2500 Component \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
3535
|
-
|
|
3536
|
-
/**
|
|
3537
|
-
* ShrimpRun \u2014 Default Rakta.js interactive starter game.
|
|
3538
|
-
*
|
|
3539
|
-
* Like the Chrome offline Dino game, but the dinosaur is an animated shrimp.
|
|
3540
|
-
* Press Space or click the game canvas to jump. Avoid the red obstacles!
|
|
3541
|
-
*
|
|
3542
|
-
* Features:
|
|
3543
|
-
* - React state only \u2014 no external game library
|
|
3544
|
-
* - requestAnimationFrame game loop
|
|
3545
|
-
* - Physics: gravity + jump velocity
|
|
3546
|
-
* - Score that increases over time
|
|
3547
|
-
* - Speed ramps up as score grows
|
|
3548
|
-
* - Collision detection with margin
|
|
3549
|
-
* - High score tracked in component state
|
|
3550
|
-
* - Keyboard (Space) and click/tap support
|
|
3551
|
-
* - Accessible button game canvas
|
|
3552
|
-
* - SVG shrimp mascot \u2014 no external assets
|
|
3553
|
-
*/
|
|
3554
|
-
export default function ShrimpRunGame() {
|
|
3555
|
-
const [gameStatus, setGameStatus] = useState<GameStatus>("idle");
|
|
3556
|
-
const [score, setScore] = useState(0);
|
|
3557
|
-
const [highScore, setHighScore] = useState(0);
|
|
3558
|
-
const [obstacles, setObstacles] = useState<ObstacleState[]>([]);
|
|
3559
|
-
const [shrimp, setShrimp] = useState<ShrimpState>({
|
|
3560
|
-
yPosition: 0,
|
|
3561
|
-
velocityY: 0,
|
|
3562
|
-
isJumping: false,
|
|
3563
|
-
});
|
|
3564
|
-
|
|
3565
|
-
const gameStatusRef = useRef<GameStatus>("idle");
|
|
3566
|
-
const scoreRef = useRef(0);
|
|
3567
|
-
const shrimpRef = useRef<ShrimpState>({
|
|
3568
|
-
yPosition: 0,
|
|
3569
|
-
velocityY: 0,
|
|
3570
|
-
isJumping: false,
|
|
3571
|
-
});
|
|
3572
|
-
const obstaclesRef = useRef<ObstacleState[]>([]);
|
|
3573
|
-
const obstacleIdRef = useRef(0);
|
|
3574
|
-
const animationFrameRef = useRef<number>(0);
|
|
3575
|
-
const lastObstacleTimeRef = useRef(0);
|
|
3576
|
-
const lastScoreTickRef = useRef(0);
|
|
3577
|
-
|
|
3578
|
-
const jump = useCallback((): void => {
|
|
3579
|
-
if (gameStatusRef.current === "dead") {
|
|
3580
|
-
return;
|
|
3581
|
-
}
|
|
3582
|
-
|
|
3583
|
-
if (gameStatusRef.current === "idle") {
|
|
3584
|
-
gameStatusRef.current = "running";
|
|
3585
|
-
setGameStatus("running");
|
|
3586
|
-
}
|
|
3587
|
-
|
|
3588
|
-
if (!shrimpRef.current.isJumping) {
|
|
3589
|
-
const nextShrimp: ShrimpState = {
|
|
3590
|
-
...shrimpRef.current,
|
|
3591
|
-
velocityY: JUMP_VELOCITY,
|
|
3592
|
-
isJumping: true,
|
|
3593
|
-
};
|
|
3594
|
-
|
|
3595
|
-
shrimpRef.current = nextShrimp;
|
|
3596
|
-
setShrimp(nextShrimp);
|
|
3597
|
-
}
|
|
3598
|
-
}, []);
|
|
3599
|
-
|
|
3600
|
-
const resetGame = useCallback((): void => {
|
|
3601
|
-
const freshShrimp: ShrimpState = {
|
|
3602
|
-
yPosition: 0,
|
|
3603
|
-
velocityY: 0,
|
|
3604
|
-
isJumping: false,
|
|
3605
|
-
};
|
|
3606
|
-
|
|
3607
|
-
shrimpRef.current = freshShrimp;
|
|
3608
|
-
obstaclesRef.current = [];
|
|
3609
|
-
obstacleIdRef.current = 0;
|
|
3610
|
-
scoreRef.current = 0;
|
|
3611
|
-
gameStatusRef.current = "idle";
|
|
3612
|
-
lastObstacleTimeRef.current = 0;
|
|
3613
|
-
lastScoreTickRef.current = 0;
|
|
3614
|
-
|
|
3615
|
-
setShrimp(freshShrimp);
|
|
3616
|
-
setObstacles([]);
|
|
3617
|
-
setScore(0);
|
|
3618
|
-
setGameStatus("idle");
|
|
3619
|
-
}, []);
|
|
3620
|
-
|
|
3621
|
-
useEffect(() => {
|
|
3622
|
-
let previousTimestamp = 0;
|
|
3623
|
-
|
|
3624
|
-
function gameTick(timestamp: number): void {
|
|
3625
|
-
if (gameStatusRef.current !== "running") {
|
|
3626
|
-
animationFrameRef.current = requestAnimationFrame(gameTick);
|
|
3627
|
-
return;
|
|
3628
|
-
}
|
|
3629
|
-
|
|
3630
|
-
const deltaTime = timestamp - previousTimestamp;
|
|
3631
|
-
previousTimestamp = timestamp;
|
|
3632
|
-
|
|
3633
|
-
if (deltaTime > FRAME_SKIP_THRESHOLD_MS) {
|
|
3634
|
-
animationFrameRef.current = requestAnimationFrame(gameTick);
|
|
3635
|
-
return;
|
|
3636
|
-
}
|
|
3637
|
-
|
|
3638
|
-
const currentShrimp = shrimpRef.current;
|
|
3639
|
-
let nextVelocityY = currentShrimp.velocityY + GRAVITY;
|
|
3640
|
-
let nextYPosition = currentShrimp.yPosition - nextVelocityY;
|
|
3641
|
-
|
|
3642
|
-
if (nextYPosition <= 0) {
|
|
3643
|
-
nextYPosition = 0;
|
|
3644
|
-
nextVelocityY = 0;
|
|
3645
|
-
}
|
|
3646
|
-
|
|
3647
|
-
const nextShrimp: ShrimpState = {
|
|
3648
|
-
yPosition: nextYPosition,
|
|
3649
|
-
velocityY: nextVelocityY,
|
|
3650
|
-
isJumping: nextYPosition > 0,
|
|
3651
|
-
};
|
|
3652
|
-
|
|
3653
|
-
shrimpRef.current = nextShrimp;
|
|
3654
|
-
setShrimp(nextShrimp);
|
|
3655
|
-
|
|
3656
|
-
const obstacleSpeed = getObstacleSpeed(scoreRef.current);
|
|
3657
|
-
|
|
3658
|
-
const movedObstacles = obstaclesRef.current
|
|
3659
|
-
.map(
|
|
3660
|
-
(obstacle): ObstacleState => ({
|
|
3661
|
-
...obstacle,
|
|
3662
|
-
xPosition: obstacle.xPosition - obstacleSpeed,
|
|
3663
|
-
}),
|
|
3664
|
-
)
|
|
3665
|
-
.filter((obstacle) => obstacle.xPosition + obstacle.width > -10);
|
|
3666
|
-
|
|
3667
|
-
if (
|
|
3668
|
-
timestamp - lastObstacleTimeRef.current > OBSTACLE_SPAWN_INTERVAL_MS &&
|
|
3669
|
-
movedObstacles.length < MAX_CONCURRENT_OBSTACLES
|
|
3670
|
-
) {
|
|
3671
|
-
const obstacleHeight = 30 + Math.floor(Math.random() * 30);
|
|
3672
|
-
const obstacleWidth = 20 + Math.floor(Math.random() * 20);
|
|
3673
|
-
|
|
3674
|
-
movedObstacles.push({
|
|
3675
|
-
id: obstacleIdRef.current,
|
|
3676
|
-
xPosition: CANVAS_WIDTH + 20,
|
|
3677
|
-
width: obstacleWidth,
|
|
3678
|
-
height: obstacleHeight,
|
|
3679
|
-
});
|
|
3680
|
-
|
|
3681
|
-
obstacleIdRef.current += 1;
|
|
3682
|
-
lastObstacleTimeRef.current = timestamp;
|
|
3683
|
-
}
|
|
3684
|
-
|
|
3685
|
-
obstaclesRef.current = movedObstacles;
|
|
3686
|
-
setObstacles([...movedObstacles]);
|
|
3687
|
-
|
|
3688
|
-
for (const obstacle of movedObstacles) {
|
|
3689
|
-
if (checkCollision(nextShrimp.yPosition, obstacle)) {
|
|
3690
|
-
gameStatusRef.current = "dead";
|
|
3691
|
-
setGameStatus("dead");
|
|
3692
|
-
setHighScore((previousHighScore: number) =>
|
|
3693
|
-
Math.max(previousHighScore, scoreRef.current),
|
|
3694
|
-
);
|
|
3695
|
-
animationFrameRef.current = requestAnimationFrame(gameTick);
|
|
3696
|
-
return;
|
|
3697
|
-
}
|
|
3698
|
-
}
|
|
3699
|
-
|
|
3700
|
-
if (timestamp - lastScoreTickRef.current > SCORE_TICK_MS) {
|
|
3701
|
-
scoreRef.current += 1;
|
|
3702
|
-
setScore(scoreRef.current);
|
|
3703
|
-
lastScoreTickRef.current = timestamp;
|
|
3704
|
-
}
|
|
3705
|
-
|
|
3706
|
-
animationFrameRef.current = requestAnimationFrame(gameTick);
|
|
3707
|
-
}
|
|
3708
|
-
|
|
3709
|
-
animationFrameRef.current = requestAnimationFrame(gameTick);
|
|
3710
|
-
|
|
3711
|
-
return () => {
|
|
3712
|
-
cancelAnimationFrame(animationFrameRef.current);
|
|
3713
|
-
};
|
|
3714
|
-
}, []);
|
|
3715
|
-
|
|
3716
|
-
useEffect(() => {
|
|
3717
|
-
function handleKeyDown(keyboardEvent: KeyboardEvent): void {
|
|
3718
|
-
if (keyboardEvent.code === "Space") {
|
|
3719
|
-
keyboardEvent.preventDefault();
|
|
3720
|
-
jump();
|
|
3721
|
-
}
|
|
3722
|
-
}
|
|
3723
|
-
|
|
3724
|
-
window.addEventListener("keydown", handleKeyDown);
|
|
3725
|
-
|
|
3726
|
-
return () => {
|
|
3727
|
-
window.removeEventListener("keydown", handleKeyDown);
|
|
3728
|
-
};
|
|
3729
|
-
}, [jump]);
|
|
3730
|
-
|
|
3731
|
-
const shrimpBottomOffset = GROUND_STRIP_HEIGHT + shrimp.yPosition;
|
|
3732
|
-
const isDead = gameStatus === "dead";
|
|
3733
|
-
const isIdle = gameStatus === "idle";
|
|
3734
|
-
const isRunning = gameStatus === "running";
|
|
3735
|
-
|
|
3736
|
-
return (
|
|
3737
|
-
<div className="flex flex-col items-start gap-4 py-4">
|
|
3738
|
-
<div className="flex flex-wrap items-center gap-8">
|
|
3739
|
-
<span className="font-mono text-xl font-bold tabular-nums text-red-600">
|
|
3740
|
-
Score: {score}
|
|
3741
|
-
</span>
|
|
3742
|
-
{highScore > 0 && (
|
|
3743
|
-
<span className="text-sm text-slate-400">Best: {highScore}</span>
|
|
3744
|
-
)}
|
|
3745
|
-
</div>
|
|
3746
|
-
|
|
3747
|
-
<button
|
|
3748
|
-
type="button"
|
|
3749
|
-
className="relative block max-w-full cursor-pointer select-none overflow-hidden rounded-2xl border-2 border-red-600/30 bg-[#0e111a] p-0 text-left focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-red-500"
|
|
3750
|
-
aria-label="ShrimpRun game area. Click or press Space to jump."
|
|
3751
|
-
onClick={jump}
|
|
3752
|
-
onKeyDown={(keyboardEvent: import("react").KeyboardEvent) => {
|
|
3753
|
-
if (keyboardEvent.code === "Space") {
|
|
3754
|
-
keyboardEvent.preventDefault();
|
|
3755
|
-
jump();
|
|
3756
|
-
}
|
|
3757
|
-
}}
|
|
3758
|
-
style={{
|
|
3759
|
-
width: CANVAS_WIDTH,
|
|
3760
|
-
height: CANVAS_HEIGHT,
|
|
3761
|
-
}}
|
|
3762
|
-
>
|
|
3763
|
-
<span
|
|
3764
|
-
className="absolute bottom-0 left-0 w-full rounded-sm bg-red-600"
|
|
3765
|
-
style={{
|
|
3766
|
-
height: GROUND_STRIP_HEIGHT,
|
|
3767
|
-
}}
|
|
3768
|
-
/>
|
|
3769
|
-
|
|
3770
|
-
<span
|
|
3771
|
-
className="absolute shrimp-sprite"
|
|
3772
|
-
style={{
|
|
3773
|
-
left: SHRIMP_START_X,
|
|
3774
|
-
bottom: shrimpBottomOffset,
|
|
3775
|
-
width: SHRIMP_WIDTH,
|
|
3776
|
-
height: SHRIMP_HEIGHT,
|
|
3777
|
-
transform: isDead
|
|
3778
|
-
? "rotate(18deg) translateY(8px)"
|
|
3779
|
-
: shrimp.isJumping
|
|
3780
|
-
? "rotate(-8deg) translateY(-4px)"
|
|
3781
|
-
: "rotate(0deg)",
|
|
3782
|
-
transition: "transform 120ms ease",
|
|
3783
|
-
}}
|
|
3784
|
-
>
|
|
3785
|
-
<RaktaShrimpMascot isJumping={shrimp.isJumping} isDead={isDead} />
|
|
3786
|
-
</span>
|
|
3787
|
-
|
|
3788
|
-
{obstacles.map((obstacle) => (
|
|
3789
|
-
<span
|
|
3790
|
-
key={obstacle.id}
|
|
3791
|
-
className="absolute shrimp-run-obstacle"
|
|
3792
|
-
style={{
|
|
3793
|
-
left: obstacle.xPosition,
|
|
3794
|
-
bottom: GROUND_STRIP_HEIGHT,
|
|
3795
|
-
width: obstacle.width,
|
|
3796
|
-
height: obstacle.height,
|
|
3797
|
-
}}
|
|
3798
|
-
/>
|
|
3799
|
-
))}
|
|
3800
|
-
|
|
3801
|
-
{isIdle && (
|
|
3802
|
-
<span className="pointer-events-none absolute inset-0 flex items-center justify-center text-sm text-slate-400">
|
|
3803
|
-
Press Space or click to start
|
|
3804
|
-
</span>
|
|
3805
|
-
)}
|
|
3806
|
-
|
|
3807
|
-
{isDead && (
|
|
3808
|
-
<span className="absolute inset-0 flex flex-col items-center justify-center gap-2 bg-black/75">
|
|
3809
|
-
<span className="text-lg font-bold tracking-widest text-red-600">
|
|
3810
|
-
GAME OVER
|
|
3811
|
-
</span>
|
|
3812
|
-
<span className="text-sm text-slate-400">Score: {score}</span>
|
|
3813
|
-
</span>
|
|
3814
|
-
)}
|
|
3815
|
-
|
|
3816
|
-
{isRunning && score > 0 && score % 50 === 0 && (
|
|
3817
|
-
<span className="absolute right-3 top-2 text-xs font-bold tracking-widest text-red-600 opacity-80">
|
|
3818
|
-
{score}!
|
|
3819
|
-
</span>
|
|
3820
|
-
)}
|
|
3821
|
-
</button>
|
|
3822
|
-
|
|
3823
|
-
<p className="min-h-5 text-sm text-slate-400">
|
|
3824
|
-
{isIdle && "\uD83E\uDD90 Click or press Space to make the shrimp jump!"}
|
|
3825
|
-
{isRunning && "\uD83E\uDD90 Don't hit the obstacles!"}
|
|
3826
|
-
{isDead && "The shrimp got cooked. Try again!"}
|
|
3827
|
-
</p>
|
|
3828
|
-
|
|
3829
|
-
{isDead && (
|
|
3830
|
-
<button
|
|
3831
|
-
type="button"
|
|
3832
|
-
className="w-fit rounded-lg bg-red-600 px-6 py-2 font-semibold text-white transition hover:bg-red-700 active:bg-red-800 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-red-500"
|
|
3833
|
-
onClick={resetGame}
|
|
3834
|
-
>
|
|
3835
|
-
Restart
|
|
3836
|
-
</button>
|
|
3837
|
-
)}
|
|
3838
|
-
</div>
|
|
3839
|
-
);
|
|
3840
|
-
}
|
|
3841
|
-
`;
|
|
3842
|
-
}
|
|
3843
3711
|
function generateFullstackHomePage(projectName) {
|
|
3844
3712
|
return `import React from "react";
|
|
3845
3713
|
|
|
@@ -3850,7 +3718,7 @@ export default function HomePage() {
|
|
|
3850
3718
|
<p className="eyebrow">THE RED ROUTER FRAMEWORK</p>
|
|
3851
3719
|
<h1>Welcome to ${projectName}</h1>
|
|
3852
3720
|
<p>
|
|
3853
|
-
Built with Rakta.js \
|
|
3721
|
+
Built with Rakta.js \xE2\u20AC\u201D Small in size. Fierce in speed. Alive in every route.
|
|
3854
3722
|
</p>
|
|
3855
3723
|
<div className="button-row">
|
|
3856
3724
|
<a href="/about">About</a>
|
|
@@ -4061,7 +3929,7 @@ function generateProjectReadme(projectConfig) {
|
|
|
4061
3929
|
if (projectMode === "frontend-only") {
|
|
4062
3930
|
return `# ${projectName}
|
|
4063
3931
|
|
|
4064
|
-
Built with Rakta.js \
|
|
3932
|
+
Built with Rakta.js \xE2\u20AC\u201D Small in size. Fierce in speed. Alive in every route.
|
|
4065
3933
|
|
|
4066
3934
|
## Stack
|
|
4067
3935
|
|
|
@@ -4080,12 +3948,12 @@ bun run dev
|
|
|
4080
3948
|
|
|
4081
3949
|
## ShrimpRun
|
|
4082
3950
|
|
|
4083
|
-
Your starter includes ShrimpRun \
|
|
3951
|
+
Your starter includes ShrimpRun \xE2\u20AC\u201D an interactive game where a shrimp dodges obstacles. Press Space or click to jump!
|
|
4084
3952
|
`;
|
|
4085
3953
|
}
|
|
4086
3954
|
return `# ${projectName}
|
|
4087
3955
|
|
|
4088
|
-
Built with Rakta.js \
|
|
3956
|
+
Built with Rakta.js \xE2\u20AC\u201D Small in size. Fierce in speed. Alive in every route.
|
|
4089
3957
|
|
|
4090
3958
|
## Stack
|
|
4091
3959
|
|
|
@@ -4200,6 +4068,24 @@ async function promptCssFramework() {
|
|
|
4200
4068
|
});
|
|
4201
4069
|
return getPromptValue(promptResult);
|
|
4202
4070
|
}
|
|
4071
|
+
async function promptProjectLanguage() {
|
|
4072
|
+
const promptResult = await select({
|
|
4073
|
+
message: "Choose a language:",
|
|
4074
|
+
options: [
|
|
4075
|
+
{
|
|
4076
|
+
value: "typescript",
|
|
4077
|
+
label: PROJECT_LANGUAGE_DISPLAY.typescript,
|
|
4078
|
+
hint: "recommended"
|
|
4079
|
+
},
|
|
4080
|
+
{
|
|
4081
|
+
value: "javascript",
|
|
4082
|
+
label: PROJECT_LANGUAGE_DISPLAY.javascript
|
|
4083
|
+
}
|
|
4084
|
+
],
|
|
4085
|
+
initialValue: "typescript"
|
|
4086
|
+
});
|
|
4087
|
+
return getPromptValue(promptResult);
|
|
4088
|
+
}
|
|
4203
4089
|
async function promptRenderMode() {
|
|
4204
4090
|
const promptResult = await select({
|
|
4205
4091
|
message: "Choose a render mode:",
|
|
@@ -4312,12 +4198,16 @@ async function promptDatabase() {
|
|
|
4312
4198
|
}
|
|
4313
4199
|
async function runPrompts(projectName) {
|
|
4314
4200
|
const projectMode = await promptProjectMode();
|
|
4201
|
+
const language = await promptProjectLanguage();
|
|
4315
4202
|
const cssFramework = await promptCssFramework();
|
|
4316
4203
|
const renderMode = await promptRenderMode();
|
|
4204
|
+
const useTypeScript = language === "typescript";
|
|
4317
4205
|
if (projectMode === "frontend-only") {
|
|
4318
4206
|
return {
|
|
4319
4207
|
projectName,
|
|
4320
4208
|
projectMode,
|
|
4209
|
+
language,
|
|
4210
|
+
useTypeScript,
|
|
4321
4211
|
cssFramework,
|
|
4322
4212
|
renderMode,
|
|
4323
4213
|
backendFramework: "gaman",
|
|
@@ -4329,6 +4219,8 @@ async function runPrompts(projectName) {
|
|
|
4329
4219
|
return {
|
|
4330
4220
|
projectName,
|
|
4331
4221
|
projectMode,
|
|
4222
|
+
language,
|
|
4223
|
+
useTypeScript,
|
|
4332
4224
|
cssFramework,
|
|
4333
4225
|
renderMode,
|
|
4334
4226
|
backendFramework,
|
|
@@ -4346,24 +4238,25 @@ function getProjectNameFromArgs(cliArgs) {
|
|
|
4346
4238
|
}
|
|
4347
4239
|
function formatFullstackCommands() {
|
|
4348
4240
|
return [
|
|
4349
|
-
|
|
4350
|
-
"cd frontend",
|
|
4351
|
-
"bun install",
|
|
4352
|
-
"bun run dev",
|
|
4241
|
+
import_picocolors.default.dim("# Terminal 1"),
|
|
4242
|
+
import_picocolors.default.cyan("cd frontend"),
|
|
4243
|
+
import_picocolors.default.cyan("bun install"),
|
|
4244
|
+
import_picocolors.default.cyan("bun run dev"),
|
|
4353
4245
|
"",
|
|
4354
|
-
|
|
4355
|
-
"cd backend",
|
|
4356
|
-
"bun install",
|
|
4357
|
-
"bun run dev"
|
|
4358
|
-
].join(`
|
|
4359
|
-
|
|
4246
|
+
import_picocolors.default.dim("# Terminal 2"),
|
|
4247
|
+
import_picocolors.default.cyan("cd backend"),
|
|
4248
|
+
import_picocolors.default.cyan("bun install"),
|
|
4249
|
+
import_picocolors.default.cyan("bun run dev")
|
|
4250
|
+
].map((line) => line.length === 0 ? "" : ` ${line}`).join(`
|
|
4251
|
+
`);
|
|
4360
4252
|
}
|
|
4361
4253
|
function formatFrontendOnlyCommands(projectName) {
|
|
4362
|
-
return [`cd ${projectName}`, "bun install", "bun run dev"].join(`
|
|
4363
|
-
|
|
4254
|
+
return [`cd ${projectName}`, "bun install", "bun run dev"].map((command) => ` ${import_picocolors.default.cyan(command)}`).join(`
|
|
4255
|
+
`);
|
|
4364
4256
|
}
|
|
4365
4257
|
function printSuccessMessage(projectConfig) {
|
|
4366
4258
|
const modeLabel = PROJECT_MODE_DISPLAY[projectConfig.projectMode];
|
|
4259
|
+
const languageLabel = PROJECT_LANGUAGE_DISPLAY[projectConfig.language];
|
|
4367
4260
|
const cssLabel = CSS_DISPLAY[projectConfig.cssFramework];
|
|
4368
4261
|
const renderLabel = RENDER_MODE_DISPLAY[projectConfig.renderMode];
|
|
4369
4262
|
const isFullstack = projectConfig.projectMode === "fullstack";
|
|
@@ -4374,6 +4267,7 @@ function printSuccessMessage(projectConfig) {
|
|
|
4374
4267
|
${import_picocolors.default.bold(import_picocolors.default.green("Project created!"))}
|
|
4375
4268
|
|
|
4376
4269
|
${import_picocolors.default.dim("Mode:")} ${modeLabel}
|
|
4270
|
+
${import_picocolors.default.dim("Lang:")} ${languageLabel}
|
|
4377
4271
|
${import_picocolors.default.dim("CSS:")} ${cssLabel}
|
|
4378
4272
|
${import_picocolors.default.dim("Render:")} ${renderLabel}
|
|
4379
4273
|
${backendLine}
|
|
@@ -4381,7 +4275,7 @@ function printSuccessMessage(projectConfig) {
|
|
|
4381
4275
|
|
|
4382
4276
|
${import_picocolors.default.bold("Next steps:")}
|
|
4383
4277
|
|
|
4384
|
-
|
|
4278
|
+
${nextSteps}
|
|
4385
4279
|
|
|
4386
4280
|
${import_picocolors.default.bold("Frontend:")} ${import_picocolors.default.cyan("http://localhost:3000")}
|
|
4387
4281
|
${isFullstack ? `${import_picocolors.default.bold("Backend:")} ${import_picocolors.default.cyan("http://localhost:4000")}` : ""}
|
|
@@ -4421,4 +4315,4 @@ Error: ${errorMessage}
|
|
|
4421
4315
|
process.exit(1);
|
|
4422
4316
|
});
|
|
4423
4317
|
|
|
4424
|
-
//# debugId=
|
|
4318
|
+
//# debugId=F17FA4C670BBE80264756E2164756E21
|