1
0
Fork 0

Refactor no single games monorepo, add building & deploy

This commit is contained in:
Pabloader 2024-07-06 17:26:15 +00:00
parent 99ed5dd8c8
commit e6e3a38328
28 changed files with 206 additions and 79 deletions

BIN
bun.lockb

Binary file not shown.

View File

@ -1,9 +1,10 @@
{ {
"name": "binario", "name": "tsgames",
"module": "index.ts", "module": "index.ts",
"type": "module", "type": "module",
"scripts": { "scripts": {
"start": "bun --hot src/server.ts" "start": "bun --hot src/build/server.ts",
"build": "bun src/build/build.ts"
}, },
"dependencies": { "dependencies": {
"classnames": "2.5.1", "classnames": "2.5.1",
@ -11,7 +12,9 @@
}, },
"devDependencies": { "devDependencies": {
"@types/bun": "latest", "@types/bun": "latest",
"@types/inquirer": "9.0.7",
"bun-lightningcss": "0.2.0", "bun-lightningcss": "0.2.0",
"inquirer": "9.3.4",
"typescript": "5.5.2" "typescript": "5.5.2"
} }
} }

View File

@ -3,14 +3,8 @@
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Binario</title> <title>$TITLE$</title>
<style> <style>
:root {
--slot-size: 64px;
--color-bg-select: rgba(0, 0, 0, 0.1);
--color-bg-select: rgba(0, 200, 0, 0.1);
--color-border-select: rgb(0, 200, 0);
}
html, body, #c { html, body, #c {
width: 100vw; width: 100vw;
height: 100vh; height: 100vh;
@ -35,8 +29,7 @@
</style> </style>
</head> </head>
<body> <body>
<canvas id="c"></canvas> <canvas id="canvas"></canvas>
<div id="controls"></div>
$SCRIPT$ $SCRIPT$
</body> </body>
</html> </html>

34
src/build/build.ts Normal file
View File

@ -0,0 +1,34 @@
import { $ } from 'bun';
import path from 'path';
import fs from 'fs/promises';
import { buildHTML } from "./html";
import inquirer from 'inquirer';
import { isGame, getGames } from './isGame';
const outDir = path.resolve(import.meta.dir, '..', '..', 'dist');
await fs.mkdir(outDir, { recursive: true });
let game = process.argv[2];
while (!await isGame(game)) {
const answer = await inquirer.prompt([{
type: 'list',
name: 'game',
choices: await getGames(),
}]);
game = answer.game;
}
const html = await buildHTML(game, true);
if (!html) {
process.exit(1);
}
const filePath = path.resolve(outDir, `${game}.html`);
await Bun.write(filePath, html);
const result = await $`scp "${filePath}" "pabloid@games.pafnooty.ru:/var/www/games/${game}.html"`;
if (result.exitCode === 0) {
console.log(`Build successful: https://games.pafnooty.ru/${game}.html`);
}

32
src/build/html.ts Normal file
View File

@ -0,0 +1,32 @@
import path from 'path';
import dataUrlPlugin from './dataUrlPlugin';
import lightningcss from 'bun-lightningcss';
import { getGames } from './isGame';
export async function buildHTML(game: string, production = false) {
const html = await Bun.file(path.resolve(import.meta.dir, '..', 'assets', 'index.html')).text();
const bundle = await Bun.build({
entrypoints: [path.resolve(import.meta.dir, '..', 'index.ts')],
sourcemap: production ? 'none' : 'inline',
minify: production,
define: {
global: 'window',
GAME: `"${game}"`,
GAMES: JSON.stringify(await getGames()),
},
plugins: [
dataUrlPlugin,
lightningcss(),
]
});
if (bundle.success && bundle.outputs.length === 1) {
const script = await bundle.outputs[0].text();
return html
.replace('$SCRIPT$', `<script>${script}</script>`)
.replace('$TITLE$', game[0].toUpperCase() + game.slice(1).toLowerCase());
} else {
console.error('Multiple assets: ', bundle.outputs, 'or fail: ', !bundle.success, bundle);
}
}

25
src/build/isGame.ts Normal file
View File

@ -0,0 +1,25 @@
import path from 'path';
import fs from 'fs/promises';
export async function isGame(name: string | null | undefined) {
if (!name || name === 'index') return false;
const dir = path.resolve(import.meta.dir, '..', 'games', name);
if (!await fs.exists(dir)) return false;
const stat = await fs.stat(dir);
return stat.isDirectory();
}
export async function getGames() {
const dir = path.resolve(import.meta.dir, '..', 'games');
if (!await fs.exists(dir)) return [];
const stat = await fs.stat(dir);
if (!stat.isDirectory()) return [];
const list = await fs.readdir(dir);
return list.filter(d => d !== 'index');
}

35
src/build/server.ts Normal file
View File

@ -0,0 +1,35 @@
import Bun from 'bun';
import path from 'path';
import { buildHTML } from './html';
import { isGame } from './isGame';
Bun.serve({
async fetch(req) {
const url = new URL(req.url);
const pathname = path.basename(url.pathname);
const gameParam = url.searchParams.get('game');
const game = (gameParam && await isGame(gameParam)) ? gameParam : 'index';
switch (pathname) {
case '':
case '/':
case 'index.html':
try {
const html = await buildHTML(game);
if (html) {
return new Response(html, {
headers: {
'Content-Type': 'text/html;charset=utf-8'
}
});
}
} catch (e) {
console.error(e);
}
return new Response(null, { status: 500 });
default:
console.log(`Pathname: ${pathname}`);
return new Response(null, { status: 404 });
}
}
})

View File

Before

Width:  |  Height:  |  Size: 206 B

After

Width:  |  Height:  |  Size: 206 B

View File

Before

Width:  |  Height:  |  Size: 233 B

After

Width:  |  Height:  |  Size: 233 B

View File

Before

Width:  |  Height:  |  Size: 157 B

After

Width:  |  Height:  |  Size: 157 B

View File

Before

Width:  |  Height:  |  Size: 163 B

After

Width:  |  Height:  |  Size: 163 B

View File

Before

Width:  |  Height:  |  Size: 195 B

After

Width:  |  Height:  |  Size: 195 B

View File

Before

Width:  |  Height:  |  Size: 196 B

After

Width:  |  Height:  |  Size: 196 B

View File

Before

Width:  |  Height:  |  Size: 241 B

After

Width:  |  Height:  |  Size: 241 B

View File

Before

Width:  |  Height:  |  Size: 186 B

After

Width:  |  Height:  |  Size: 186 B

View File

@ -1,3 +1,10 @@
:root {
--slot-size: 64px;
--color-bg-select: rgba(0, 0, 0, 0.1);
--color-bg-select: rgba(0, 200, 0, 0.1);
--color-border-select: rgb(0, 200, 0);
}
.button { .button {
display: flex; display: flex;
justify-content: center; justify-content: center;

View File

@ -3,7 +3,7 @@ import UI from "./ui";
import World from "./world"; import World from "./world";
import { pointsEquals, prevent } from "./utils"; import { pointsEquals, prevent } from "./utils";
export default class Game { export default class Binario implements IGame {
private running = false; private running = false;
private mouseDown: false | number = false; private mouseDown: false | number = false;
private graphics; private graphics;
@ -13,8 +13,7 @@ export default class Game {
private prevFrame: number = performance.now(); private prevFrame: number = performance.now();
private paused = false; private paused = false;
constructor(private canvas: HTMLCanvasElement, controls: HTMLElement) { constructor(private canvas: HTMLCanvasElement) {
canvas.focus(); canvas.focus();
canvas.addEventListener('wheel', this.onScroll); canvas.addEventListener('wheel', this.onScroll);
@ -28,7 +27,7 @@ export default class Game {
this.graphics = new Graphics(canvas); this.graphics = new Graphics(canvas);
this.world = new World(); this.world = new World();
this.ui = new UI(controls); this.ui = new UI();
window.addEventListener('resize', this.onResize); window.addEventListener('resize', this.onResize);
this.onResize(); this.onResize();
@ -38,7 +37,7 @@ export default class Game {
private onResize = () => { private onResize = () => {
this.canvas.width = window.innerWidth; this.canvas.width = window.innerWidth;
this.canvas.height = window.innerHeight; this.canvas.height = window.innerHeight;
this.graphics.resetStyle(); this.graphics.resetStyle();
} }

View File

@ -2,11 +2,11 @@ import { type Tile, TileType, getPortDirections, PortDirection, LIMITS, type Res
import { ALL_DIRECTIONS, Direction, makeImage, movePoint } from "./utils"; import { ALL_DIRECTIONS, Direction, makeImage, movePoint } from "./utils";
import emptySrc from '../assets/img/empty.png'; import emptySrc from './assets/img/empty.png';
import extractorSrc from '../assets/img/extractor.png'; import extractorSrc from './assets/img/extractor.png';
import notSrc from '../assets/img/not.png'; import notSrc from './assets/img/not.png';
import andSrc from '../assets/img/and.png'; import andSrc from './assets/img/and.png';
import orSrc from '../assets/img/or.png'; import orSrc from './assets/img/or.png';
export interface ViewConfig { export interface ViewConfig {
tileSize: number; tileSize: number;

View File

@ -3,12 +3,12 @@ import cn from 'classnames';
import { range } from './utils'; import { range } from './utils';
import { TileType } from './world'; import { TileType } from './world';
import styles from '../assets/ui.module.css'; import styles from './assets/ui.module.css';
import conveyorSrc from '../assets/img/conveyor.png'; import conveyorSrc from './assets/img/conveyor.png';
import extractorSrc from '../assets/img/extractor.png'; import extractorSrc from './assets/img/extractor.png';
import notSrc from '../assets/img/not.png'; import notSrc from './assets/img/not.png';
import andSrc from '../assets/img/and.png'; import andSrc from './assets/img/and.png';
import orSrc from '../assets/img/or.png'; import orSrc from './assets/img/or.png';
export enum ToolType { export enum ToolType {
SELECT, SELECT,
@ -64,9 +64,14 @@ const TOOLS: (Tool | null)[] = [
]; ];
export default class UI { export default class UI {
private root: HTMLElement;
private currentTool: Tool = TOOLS[0]!; private currentTool: Tool = TOOLS[0]!;
constructor(private root: HTMLElement) { constructor() {
this.root = document.createElement('div');
this.root.id = 'controls';
document.body.appendChild(this.root);
this.render(); this.render();
} }

View File

@ -0,0 +1,11 @@
body {
display: flex;
}
.games {
display: flex;
flex-direction: column;
width: 100%;
padding: 40px 20%;
gap: 20px;
}

17
src/games/index/game.tsx Normal file
View File

@ -0,0 +1,17 @@
import { render } from "preact";
import styles from './game.module.css';
declare const GAMES: string[];
export default class GameIndex implements IGame {
constructor(canvas: HTMLCanvasElement) {
canvas.remove();
}
run() {
const root = <div class={styles.games}>
{GAMES.map(g => <a key={g} href={`?game=${g}`}>{g}</a>)}
</div>;
render(root, document.body);
}
}

View File

@ -1,14 +1,17 @@
import Game from './game/game'; declare const GAME: string;
async function main() { async function main() {
const canvas = document.getElementById('c'); const { default: Game }: { default: IGameConstructor } = await import(`./games/${GAME}/game`);
const controls = document.getElementById('controls'); const canvas = document.getElementById('canvas');
if (canvas instanceof HTMLCanvasElement && controls instanceof HTMLElement) { if (canvas instanceof HTMLCanvasElement) {
const game = new Game(canvas, controls); const game = new Game(canvas);
game.run(); game.run();
} else { } else {
alert('Something wrong with your canvas!'); alert('Something wrong with your canvas!');
} }
} }
main().catch(console.error); main().catch((e) => {
console.error(e);
// TODO display error page
});

View File

@ -1,45 +0,0 @@
import Bun from 'bun';
import path from 'path';
import dataUrlPlugin from './dataUrlPlugin';
import lightningcss from 'bun-lightningcss'
Bun.serve({
async fetch(req) {
const url = new URL(req.url);
const pathname = path.basename(url.pathname);
switch (pathname) {
case '':
case '/':
case 'index.html':
const html = await Bun.file(path.resolve(import.meta.dir, 'assets', 'index.html')).text();
const bundle = await Bun.build({
entrypoints: [path.resolve(import.meta.dir, 'index.ts')],
sourcemap: 'inline',
// minify: true,
define: {
global: 'window',
},
plugins: [
dataUrlPlugin,
lightningcss(),
]
});
if (bundle.success && bundle.outputs.length === 1) {
const script = await bundle.outputs[0].text();
return new Response(html.replace('$SCRIPT$', `<script>${script}</script>`), {
headers: {
'Content-Type': 'text/html;charset=utf-8'
}
});
} else {
console.error('Multiple assets: ', bundle.outputs, 'or fail: ', !bundle.success, bundle);
}
return new Response(null, { status: 500 });
default:
console.log(`Pathname: ${pathname}`);
return new Response(null, { status: 404 });
}
}
})

8
src/types.d.ts vendored
View File

@ -1,6 +1,14 @@
type Point = [number, number]; type Point = [number, number];
type Rect = [number, number, number, number]; type Rect = [number, number, number, number];
interface IGame {
run();
}
interface IGameConstructor {
new(canvas: HTMLCanvasElement): IGame;
}
declare module "*.png" { declare module "*.png" {
const content: string; const content: string;
export default content; export default content;