Refactor no single games monorepo, add building & deploy
|
|
@ -1,9 +1,10 @@
|
|||
{
|
||||
"name": "binario",
|
||||
"name": "tsgames",
|
||||
"module": "index.ts",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"start": "bun --hot src/server.ts"
|
||||
"start": "bun --hot src/build/server.ts",
|
||||
"build": "bun src/build/build.ts"
|
||||
},
|
||||
"dependencies": {
|
||||
"classnames": "2.5.1",
|
||||
|
|
@ -11,7 +12,9 @@
|
|||
},
|
||||
"devDependencies": {
|
||||
"@types/bun": "latest",
|
||||
"@types/inquirer": "9.0.7",
|
||||
"bun-lightningcss": "0.2.0",
|
||||
"inquirer": "9.3.4",
|
||||
"typescript": "5.5.2"
|
||||
}
|
||||
}
|
||||
|
|
@ -3,14 +3,8 @@
|
|||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Binario</title>
|
||||
<title>$TITLE$</title>
|
||||
<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 {
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
|
|
@ -35,8 +29,7 @@
|
|||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<canvas id="c"></canvas>
|
||||
<div id="controls"></div>
|
||||
<canvas id="canvas"></canvas>
|
||||
$SCRIPT$
|
||||
</body>
|
||||
</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 {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
|
|
@ -3,7 +3,7 @@ import UI from "./ui";
|
|||
import World from "./world";
|
||||
import { pointsEquals, prevent } from "./utils";
|
||||
|
||||
export default class Game {
|
||||
export default class Binario implements IGame {
|
||||
private running = false;
|
||||
private mouseDown: false | number = false;
|
||||
private graphics;
|
||||
|
|
@ -13,8 +13,7 @@ export default class Game {
|
|||
private prevFrame: number = performance.now();
|
||||
private paused = false;
|
||||
|
||||
constructor(private canvas: HTMLCanvasElement, controls: HTMLElement) {
|
||||
|
||||
constructor(private canvas: HTMLCanvasElement) {
|
||||
canvas.focus();
|
||||
|
||||
canvas.addEventListener('wheel', this.onScroll);
|
||||
|
|
@ -28,7 +27,7 @@ export default class Game {
|
|||
|
||||
this.graphics = new Graphics(canvas);
|
||||
this.world = new World();
|
||||
this.ui = new UI(controls);
|
||||
this.ui = new UI();
|
||||
|
||||
window.addEventListener('resize', this.onResize);
|
||||
this.onResize();
|
||||
|
|
@ -2,11 +2,11 @@ import { type Tile, TileType, getPortDirections, PortDirection, LIMITS, type Res
|
|||
|
||||
import { ALL_DIRECTIONS, Direction, makeImage, movePoint } from "./utils";
|
||||
|
||||
import emptySrc from '../assets/img/empty.png';
|
||||
import extractorSrc from '../assets/img/extractor.png';
|
||||
import notSrc from '../assets/img/not.png';
|
||||
import andSrc from '../assets/img/and.png';
|
||||
import orSrc from '../assets/img/or.png';
|
||||
import emptySrc from './assets/img/empty.png';
|
||||
import extractorSrc from './assets/img/extractor.png';
|
||||
import notSrc from './assets/img/not.png';
|
||||
import andSrc from './assets/img/and.png';
|
||||
import orSrc from './assets/img/or.png';
|
||||
|
||||
export interface ViewConfig {
|
||||
tileSize: number;
|
||||
|
|
@ -3,12 +3,12 @@ import cn from 'classnames';
|
|||
import { range } from './utils';
|
||||
import { TileType } from './world';
|
||||
|
||||
import styles from '../assets/ui.module.css';
|
||||
import conveyorSrc from '../assets/img/conveyor.png';
|
||||
import extractorSrc from '../assets/img/extractor.png';
|
||||
import notSrc from '../assets/img/not.png';
|
||||
import andSrc from '../assets/img/and.png';
|
||||
import orSrc from '../assets/img/or.png';
|
||||
import styles from './assets/ui.module.css';
|
||||
import conveyorSrc from './assets/img/conveyor.png';
|
||||
import extractorSrc from './assets/img/extractor.png';
|
||||
import notSrc from './assets/img/not.png';
|
||||
import andSrc from './assets/img/and.png';
|
||||
import orSrc from './assets/img/or.png';
|
||||
|
||||
export enum ToolType {
|
||||
SELECT,
|
||||
|
|
@ -64,9 +64,14 @@ const TOOLS: (Tool | null)[] = [
|
|||
];
|
||||
|
||||
export default class UI {
|
||||
private root: HTMLElement;
|
||||
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();
|
||||
}
|
||||
|
||||
|
|
@ -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() {
|
||||
const canvas = document.getElementById('c');
|
||||
const controls = document.getElementById('controls');
|
||||
if (canvas instanceof HTMLCanvasElement && controls instanceof HTMLElement) {
|
||||
const game = new Game(canvas, controls);
|
||||
const { default: Game }: { default: IGameConstructor } = await import(`./games/${GAME}/game`);
|
||||
const canvas = document.getElementById('canvas');
|
||||
if (canvas instanceof HTMLCanvasElement) {
|
||||
const game = new Game(canvas);
|
||||
game.run();
|
||||
} else {
|
||||
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 Rect = [number, number, number, number];
|
||||
|
||||
interface IGame {
|
||||
run();
|
||||
}
|
||||
|
||||
interface IGameConstructor {
|
||||
new(canvas: HTMLCanvasElement): IGame;
|
||||
}
|
||||
|
||||
declare module "*.png" {
|
||||
const content: string;
|
||||
export default content;
|
||||
|
|
|
|||