diff --git a/bun.lockb b/bun.lockb index 91b0e3b..d97726d 100755 Binary files a/bun.lockb and b/bun.lockb differ diff --git a/package.json b/package.json index 280514a..14d1839 100644 --- a/package.json +++ b/package.json @@ -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" } } \ No newline at end of file diff --git a/src/assets/index.html b/src/assets/index.html index 983cdce..59becd0 100644 --- a/src/assets/index.html +++ b/src/assets/index.html @@ -3,14 +3,8 @@ - Binario + $TITLE$ - -
+ $SCRIPT$ \ No newline at end of file diff --git a/src/build/build.ts b/src/build/build.ts new file mode 100644 index 0000000..6006b11 --- /dev/null +++ b/src/build/build.ts @@ -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`); +} \ No newline at end of file diff --git a/src/dataUrlPlugin.ts b/src/build/dataUrlPlugin.ts similarity index 100% rename from src/dataUrlPlugin.ts rename to src/build/dataUrlPlugin.ts diff --git a/src/build/html.ts b/src/build/html.ts new file mode 100644 index 0000000..6dc39f6 --- /dev/null +++ b/src/build/html.ts @@ -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$', ``) + .replace('$TITLE$', game[0].toUpperCase() + game.slice(1).toLowerCase()); + } else { + console.error('Multiple assets: ', bundle.outputs, 'or fail: ', !bundle.success, bundle); + } +} \ No newline at end of file diff --git a/src/build/isGame.ts b/src/build/isGame.ts new file mode 100644 index 0000000..081d277 --- /dev/null +++ b/src/build/isGame.ts @@ -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'); +} \ No newline at end of file diff --git a/src/build/server.ts b/src/build/server.ts new file mode 100644 index 0000000..d5329d2 --- /dev/null +++ b/src/build/server.ts @@ -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 }); + } + } +}) \ No newline at end of file diff --git a/src/assets/img/and.png b/src/games/binario/assets/img/and.png similarity index 100% rename from src/assets/img/and.png rename to src/games/binario/assets/img/and.png diff --git a/src/assets/img/conveyor.png b/src/games/binario/assets/img/conveyor.png similarity index 100% rename from src/assets/img/conveyor.png rename to src/games/binario/assets/img/conveyor.png diff --git a/src/assets/img/empty.png b/src/games/binario/assets/img/empty.png similarity index 100% rename from src/assets/img/empty.png rename to src/games/binario/assets/img/empty.png diff --git a/src/assets/img/extractor.png b/src/games/binario/assets/img/extractor.png similarity index 100% rename from src/assets/img/extractor.png rename to src/games/binario/assets/img/extractor.png diff --git a/src/assets/img/not.png b/src/games/binario/assets/img/not.png similarity index 100% rename from src/assets/img/not.png rename to src/games/binario/assets/img/not.png diff --git a/src/assets/img/or.png b/src/games/binario/assets/img/or.png similarity index 100% rename from src/assets/img/or.png rename to src/games/binario/assets/img/or.png diff --git a/src/assets/img/select.png b/src/games/binario/assets/img/select.png similarity index 100% rename from src/assets/img/select.png rename to src/games/binario/assets/img/select.png diff --git a/src/assets/img/trash.png b/src/games/binario/assets/img/trash.png similarity index 100% rename from src/assets/img/trash.png rename to src/games/binario/assets/img/trash.png diff --git a/src/assets/ui.module.css b/src/games/binario/assets/ui.module.css similarity index 85% rename from src/assets/ui.module.css rename to src/games/binario/assets/ui.module.css index d2f0655..21abc5c 100644 --- a/src/assets/ui.module.css +++ b/src/games/binario/assets/ui.module.css @@ -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; diff --git a/src/game/game.ts b/src/games/binario/game.ts similarity index 96% rename from src/game/game.ts rename to src/games/binario/game.ts index ac74e98..c095ba6 100644 --- a/src/game/game.ts +++ b/src/games/binario/game.ts @@ -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(); @@ -38,7 +37,7 @@ export default class Game { private onResize = () => { this.canvas.width = window.innerWidth; this.canvas.height = window.innerHeight; - + this.graphics.resetStyle(); } diff --git a/src/game/graphics.ts b/src/games/binario/graphics.ts similarity index 100% rename from src/game/graphics.ts rename to src/games/binario/graphics.ts diff --git a/src/game/renderer.ts b/src/games/binario/renderer.ts similarity index 95% rename from src/game/renderer.ts rename to src/games/binario/renderer.ts index 7f30f83..6e64aa6 100644 --- a/src/game/renderer.ts +++ b/src/games/binario/renderer.ts @@ -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; diff --git a/src/game/ui.tsx b/src/games/binario/ui.tsx similarity index 83% rename from src/game/ui.tsx rename to src/games/binario/ui.tsx index 132437b..1ddbdb5 100644 --- a/src/game/ui.tsx +++ b/src/games/binario/ui.tsx @@ -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(); } diff --git a/src/game/utils.ts b/src/games/binario/utils.ts similarity index 100% rename from src/game/utils.ts rename to src/games/binario/utils.ts diff --git a/src/game/world.ts b/src/games/binario/world.ts similarity index 100% rename from src/game/world.ts rename to src/games/binario/world.ts diff --git a/src/games/index/game.module.css b/src/games/index/game.module.css new file mode 100644 index 0000000..25f0caa --- /dev/null +++ b/src/games/index/game.module.css @@ -0,0 +1,11 @@ +body { + display: flex; +} + +.games { + display: flex; + flex-direction: column; + width: 100%; + padding: 40px 20%; + gap: 20px; +} \ No newline at end of file diff --git a/src/games/index/game.tsx b/src/games/index/game.tsx new file mode 100644 index 0000000..bea619d --- /dev/null +++ b/src/games/index/game.tsx @@ -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 =
+ {GAMES.map(g => {g})} +
; + + render(root, document.body); + } +} \ No newline at end of file diff --git a/src/index.ts b/src/index.ts index 9e3cc92..440fc5e 100644 --- a/src/index.ts +++ b/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); \ No newline at end of file +main().catch((e) => { + console.error(e); + // TODO display error page +}); \ No newline at end of file diff --git a/src/server.ts b/src/server.ts deleted file mode 100644 index edbbe4d..0000000 --- a/src/server.ts +++ /dev/null @@ -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$', ``), { - 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 }); - } - } -}) \ No newline at end of file diff --git a/src/types.d.ts b/src/types.d.ts index 536d762..6ffa60f 100644 --- a/src/types.d.ts +++ b/src/types.d.ts @@ -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;