Refactor no single games monorepo, add building & deploy
|
|
@ -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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -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>
|
||||||
|
|
@ -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`);
|
||||||
|
}
|
||||||
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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');
|
||||||
|
}
|
||||||
|
|
@ -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 });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
Before Width: | Height: | Size: 206 B After Width: | Height: | Size: 206 B |
|
Before Width: | Height: | Size: 233 B After Width: | Height: | Size: 233 B |
|
Before Width: | Height: | Size: 157 B After Width: | Height: | Size: 157 B |
|
Before Width: | Height: | Size: 163 B After Width: | Height: | Size: 163 B |
|
Before Width: | Height: | Size: 195 B After Width: | Height: | Size: 195 B |
|
Before Width: | Height: | Size: 196 B After Width: | Height: | Size: 196 B |
|
Before Width: | Height: | Size: 241 B After Width: | Height: | Size: 241 B |
|
Before Width: | Height: | Size: 186 B After Width: | Height: | Size: 186 B |
|
|
@ -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;
|
||||||
|
|
@ -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();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -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;
|
||||||
|
|
@ -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();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -0,0 +1,11 @@
|
||||||
|
body {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
.games {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
width: 100%;
|
||||||
|
padding: 40px 20%;
|
||||||
|
gap: 20px;
|
||||||
|
}
|
||||||
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
15
src/index.ts
|
|
@ -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
|
||||||
|
});
|
||||||
|
|
@ -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 });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||