Skip to content

Commit 3338c19

Browse files
committed
Simple vite react based pixel game
1 parent e72168e commit 3338c19

19 files changed

+761
-0
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
# Logs
2+
logs
3+
*.log
4+
npm-debug.log*
5+
yarn-debug.log*
6+
yarn-error.log*
7+
pnpm-debug.log*
8+
lerna-debug.log*
9+
10+
node_modules
11+
dist
12+
dist-ssr
13+
*.local
14+
15+
# Editor directories and files
16+
.vscode/*
17+
!.vscode/extensions.json
18+
.idea
19+
.DS_Store
20+
*.suo
21+
*.ntvs*
22+
*.njsproj
23+
*.sln
24+
*.sw?
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import js from '@eslint/js'
2+
import globals from 'globals'
3+
import reactHooks from 'eslint-plugin-react-hooks'
4+
import reactRefresh from 'eslint-plugin-react-refresh'
5+
6+
export default [
7+
{ ignores: ['dist'] },
8+
{
9+
files: ['**/*.{js,jsx}'],
10+
languageOptions: {
11+
ecmaVersion: 2020,
12+
globals: globals.browser,
13+
parserOptions: {
14+
ecmaVersion: 'latest',
15+
ecmaFeatures: { jsx: true },
16+
sourceType: 'module',
17+
},
18+
},
19+
plugins: {
20+
'react-hooks': reactHooks,
21+
'react-refresh': reactRefresh,
22+
},
23+
rules: {
24+
...js.configs.recommended.rules,
25+
...reactHooks.configs.recommended.rules,
26+
'no-unused-vars': ['error', { varsIgnorePattern: '^[A-Z_]' }],
27+
'react-refresh/only-export-components': [
28+
'warn',
29+
{ allowConstantExport: true },
30+
],
31+
},
32+
},
33+
]
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
<!doctype html>
2+
<html lang="en">
3+
<head>
4+
<meta charset="UTF-8" />
5+
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
6+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
7+
<title>Vite + React</title>
8+
</head>
9+
<body>
10+
<div id="root"></div>
11+
<script type="module" src="/src/main.jsx"></script>
12+
</body>
13+
</html>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
{
2+
"name": "reactgame",
3+
"private": true,
4+
"version": "0.0.0",
5+
"type": "module",
6+
"scripts": {
7+
"dev": "vite",
8+
"build": "vite build",
9+
"lint": "eslint .",
10+
"preview": "vite preview"
11+
},
12+
"dependencies": {
13+
"react": "^19.0.0",
14+
"react-dom": "^19.0.0"
15+
},
16+
"devDependencies": {
17+
"@eslint/js": "^9.21.0",
18+
"@types/react": "^19.0.10",
19+
"@types/react-dom": "^19.0.4",
20+
"@vitejs/plugin-react": "^4.3.4",
21+
"eslint": "^9.21.0",
22+
"eslint-plugin-react-hooks": "^5.1.0",
23+
"eslint-plugin-react-refresh": "^0.4.19",
24+
"globals": "^15.15.0",
25+
"vite": "^6.2.0"
26+
}
27+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
import fs from 'fs';
2+
import path from 'path';
3+
import { generateAsciiArtCss } from './generateAsciiArtCss.js';
4+
5+
export default function asciiArtToCssPlugin() {
6+
return {
7+
name: 'ascii-art-to-css',
8+
enforce: 'pre', // Ensure this plugin runs before other plugins
9+
handleHotUpdate({ file, server }) {
10+
if (file.endsWith('.atxt')) {
11+
const content = fs.readFileSync(file, 'utf-8');
12+
if (content.startsWith('#ascii-art')) {
13+
// Trigger a full page reload when a valid ASCII art file changes
14+
server.ws.send({ type: 'full-reload' });
15+
}
16+
}
17+
},
18+
transform(code, id) {
19+
if (id.endsWith('.atxt')) {
20+
// Extract the basename of the file (e.g., "enemy" from "enemy.atxt")
21+
const className = path.basename(id, '.atxt');
22+
23+
// Generate CSS using the utility function
24+
const cssContent = generateAsciiArtCss(code, className);
25+
26+
console.log(cssContent);
27+
28+
// During development, inject CSS dynamically
29+
if (process.env.NODE_ENV === 'development') {
30+
return `
31+
const style = document.createElement('style');
32+
style.textContent = \`${cssContent}\`;
33+
document.head.appendChild(style);
34+
`;
35+
}
36+
37+
// During production, write CSS to disk
38+
const cssFilePath = id.replace(/\.atxt$/, '.css');
39+
fs.writeFileSync(cssFilePath, cssContent, 'utf-8');
40+
41+
// Return nothing for production builds
42+
return '';
43+
}
44+
},
45+
};
46+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import fs from 'fs';
2+
import path from 'path';
3+
import { generateAsciiArtSvg } from './generateAsciiArtSvg.js';
4+
5+
export default function asciiArtToSvgPlugin() {
6+
return {
7+
name: 'ascii-art-to-svg',
8+
enforce: 'pre', // Ensure this plugin runs before other plugins
9+
transform(code, id) {
10+
if (id.endsWith('.atxt')) {
11+
// Generate the SVG string from the .atxt content
12+
const svgContent = generateAsciiArtSvg(code, 'ascii-art-svg');
13+
// Return the SVG string as a JavaScript module
14+
return `export default ${JSON.stringify(svgContent)};`;
15+
}
16+
},
17+
};
18+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
/**
2+
* Generates CSS from an .atxt file's content.
3+
* @param {string} atxtContent - The full content of the .atxt file, including the header.
4+
* @param {string} className - The class name to use in the generated CSS.
5+
* @returns {string} - The generated CSS string.
6+
* @throws {Error} - Throws an error if the size in the header is invalid.
7+
*/
8+
export function generateAsciiArtCss(atxtContent, className) {
9+
// Split the content into lines
10+
const lines = atxtContent.trim().split('\n');
11+
12+
// Parse the header
13+
const header = lines[0];
14+
if (!header.startsWith('#ascii-art')) {
15+
throw new Error('Invalid .atxt file: Missing #ascii-art header.');
16+
}
17+
18+
// Extract the size from the header (e.g., "10x10")
19+
const sizeMatch = header.match(/#ascii-art (\d+)x(\d+)/);
20+
if (!sizeMatch) {
21+
throw new Error('Invalid .atxt file: Missing or invalid size in header.');
22+
}
23+
const [_, width, height] = sizeMatch.map(Number);
24+
25+
// Extract the ASCII art content
26+
const asciiArt = lines.slice(1);
27+
28+
// Fill missing rows with blank lines to match the specified height
29+
const filledAsciiArt = Array.from({ length: height }, (_, y) => {
30+
return asciiArt[y] ? asciiArt[y].padEnd(width, ' ') : ' '.repeat(width);
31+
});
32+
33+
// Validate the dimensions of the ASCII art
34+
for (const line of filledAsciiArt) {
35+
if (line.length !== width) {
36+
throw new Error(`Invalid .atxt file: Expected each row to have ${width} columns, but got "${line}".`);
37+
}
38+
}
39+
40+
// Convert ASCII art to CSS box-shadow styles
41+
const boxShadow = filledAsciiArt
42+
.flatMap((line, y) =>
43+
line.split('').map((char, x) => {
44+
if (char === ' ') return null; // Skip empty spaces
45+
const color = char === '#' ? 'black' : 'gray'; // Define colors based on characters
46+
return `${x}px ${y}px 0 0 ${color}`;
47+
})
48+
)
49+
.filter(Boolean) // Remove null values
50+
.join(',\n '); // Format for better readability
51+
52+
// Generate the CSS content
53+
return `
54+
.${className} {
55+
width: 1px;
56+
height: 1px;
57+
box-shadow: ${boxShadow};
58+
transform: scale(10); /* Scale up the image */
59+
transform-origin: top left; /* Important for scaling */
60+
}
61+
`;
62+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
/**
2+
* Generates an SVG string from an .atxt file's content.
3+
* @param {string} atxtContent - The full content of the .atxt file, including the header.
4+
* @param {string} className - The class name to use in the generated SVG.
5+
* @returns {string} - The generated SVG string.
6+
* @throws {Error} - Throws an error if the size in the header is invalid.
7+
*/
8+
export function generateAsciiArtSvg(atxtContent, className) {
9+
// Split the content into lines
10+
const lines = atxtContent.trim().split('\n');
11+
12+
// Parse the header
13+
const header = lines[0];
14+
if (!header.startsWith('#ascii-art')) {
15+
throw new Error('Invalid .atxt file: Missing #ascii-art header.');
16+
}
17+
18+
// Extract the size from the header (e.g., "10x10")
19+
const sizeMatch = header.match(/#ascii-art (\d+)x(\d+)/);
20+
if (!sizeMatch) {
21+
throw new Error('Invalid .atxt file: Missing or invalid size in header.');
22+
}
23+
const [_, width, height] = sizeMatch.map(Number);
24+
25+
// Extract the ASCII art content
26+
const asciiArt = lines.slice(1);
27+
28+
// Fill missing rows with blank lines to match the specified height
29+
const filledAsciiArt = Array.from({ length: height }, (_, y) => {
30+
return asciiArt[y] ? asciiArt[y].padEnd(width, ' ') : ' '.repeat(width);
31+
});
32+
33+
// Generate the SVG `<rect>` elements
34+
const rects = filledAsciiArt
35+
.flatMap((line, y) =>
36+
line.split('').map((char, x) => {
37+
if (char === ' ') return null; // Skip empty spaces
38+
const color = char === '#' ? 'black' : 'gray'; // Define colors based on characters
39+
return `<rect x="${x}" y="${y}" width="1" height="1" fill="${color}" />`;
40+
})
41+
)
42+
.filter(Boolean) // Remove null values
43+
.join('\n '); // Format for better readability
44+
45+
// Generate the full SVG content
46+
return `
47+
<svg
48+
xmlns="http://www.w3.org/2000/svg"
49+
viewBox="0 0 ${width} ${height}"
50+
width="${width * 10}" // Scale width by 20x
51+
height="${height * 10}" // Scale height by 20x
52+
class="${className}"
53+
>
54+
${rects}
55+
</svg>
56+
`;
57+
}
Loading
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
#root {
2+
max-width: 1280px;
3+
margin: 0 auto;
4+
padding: 2rem;
5+
text-align: center;
6+
}
7+
8+
.logo {
9+
height: 6em;
10+
padding: 1.5em;
11+
will-change: filter;
12+
transition: filter 300ms;
13+
}
14+
.logo:hover {
15+
filter: drop-shadow(0 0 2em #646cffaa);
16+
}
17+
.logo.react:hover {
18+
filter: drop-shadow(0 0 2em #61dafbaa);
19+
}
20+
21+
@keyframes logo-spin {
22+
from {
23+
transform: rotate(0deg);
24+
}
25+
to {
26+
transform: rotate(360deg);
27+
}
28+
}
29+
30+
@media (prefers-reduced-motion: no-preference) {
31+
a:nth-of-type(2) .logo {
32+
animation: logo-spin infinite 20s linear;
33+
}
34+
}
35+
36+
.card {
37+
padding: 2em;
38+
}
39+
40+
.read-the-docs {
41+
color: #888;
42+
}

0 commit comments

Comments
 (0)