Compare commits
17 Commits
2c726c5bd2
...
5afb7023ec
| Author | SHA1 | Date | |
|---|---|---|---|
| 5afb7023ec | |||
| 419a8328fc | |||
| 68db4a923f | |||
| 6cfd69c9d3 | |||
| 7ebabb5cc3 | |||
| 1f311679f7 | |||
| f792f76f6c | |||
| d0e8f2f261 | |||
| ab8a6b02ce | |||
| 2c9f8a563e | |||
| 22cdd68184 | |||
| a8102a2730 | |||
| c2f0e0a990 | |||
| 557e2a8a8d | |||
| 5fb3b0333d | |||
| 6dde10c2ae | |||
| d320b2d74e |
86
app/nose/bin/game.tsx
Normal file
86
app/nose/bin/game.tsx
Normal file
|
|
@ -0,0 +1,86 @@
|
||||||
|
/// <reference lib="dom" />
|
||||||
|
export const game = true
|
||||||
|
|
||||||
|
import type { InputState } from "@/shared/types"
|
||||||
|
import type { GameContext } from "@/shared/game"
|
||||||
|
import { rng } from "@/shared/utils.ts"
|
||||||
|
|
||||||
|
const WIDTH = 960
|
||||||
|
const HEIGHT = 540
|
||||||
|
const SPEED = 10
|
||||||
|
let x = rng(0, WIDTH)
|
||||||
|
let y = rng(0, HEIGHT)
|
||||||
|
|
||||||
|
type Cloud = { x: number; y: number; speed: number }
|
||||||
|
const clouds: Cloud[] = [
|
||||||
|
{ x: 300, y: 100, speed: 0.7 },
|
||||||
|
{ x: 600, y: 150, speed: 0.5 },
|
||||||
|
{ x: 75, y: 220, speed: 0.5 },
|
||||||
|
]
|
||||||
|
|
||||||
|
export function init() { console.log("init") }
|
||||||
|
|
||||||
|
export function update(_delta: number, input: InputState) {
|
||||||
|
if (input.pressed.has("ArrowUp") || input.pressed.has("w")) y -= 1 * SPEED
|
||||||
|
if (input.pressed.has("ArrowDown") || input.pressed.has("s")) y += 1 * SPEED
|
||||||
|
|
||||||
|
if (input.pressed.has("ArrowLeft") || input.pressed.has("a")) x -= 1 * SPEED
|
||||||
|
if (input.pressed.has("ArrowRight") || input.pressed.has("d")) x += 1 * SPEED
|
||||||
|
|
||||||
|
for (const cloud of clouds) {
|
||||||
|
cloud.x += cloud.speed
|
||||||
|
if (cloud.x > WIDTH + 80) { // off screen
|
||||||
|
cloud.x = -80
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function draw(ctx: GameContext) {
|
||||||
|
ctx.clear()
|
||||||
|
|
||||||
|
// sky background
|
||||||
|
ctx.rectfill(0, 0, ctx.width, ctx.height, "skyblue")
|
||||||
|
|
||||||
|
// sun
|
||||||
|
ctx.circfill(800, 100, 50, "yellow")
|
||||||
|
|
||||||
|
// grass
|
||||||
|
ctx.rectfill(0, 400, ctx.width, ctx.height, "green")
|
||||||
|
|
||||||
|
// clouds
|
||||||
|
for (const cloud of clouds) {
|
||||||
|
drawCloud(ctx, cloud.x, cloud.y)
|
||||||
|
}
|
||||||
|
|
||||||
|
// house body
|
||||||
|
ctx.rectfill(200, 250, 400, 400, "sienna")
|
||||||
|
|
||||||
|
// roof (triangle-ish with lines)
|
||||||
|
ctx.trianglefill(200, 250, 400, 250, 300, 150, "brown")
|
||||||
|
|
||||||
|
// door
|
||||||
|
ctx.rectfill(280, 320, 320, 400, "darkred")
|
||||||
|
|
||||||
|
// windows
|
||||||
|
ctx.rectfill(220, 270, 260, 310, "lightblue")
|
||||||
|
ctx.rectfill(340, 270, 380, 310, "lightblue")
|
||||||
|
|
||||||
|
// player
|
||||||
|
ctx.circfill(x, y, 10, "magenta")
|
||||||
|
ctx.circ(x, y, 11, "white")
|
||||||
|
|
||||||
|
// tree trunk
|
||||||
|
ctx.rectfill(500, 300, 540, 400, "saddlebrown")
|
||||||
|
|
||||||
|
// tree top (a few circles for puffiness)
|
||||||
|
ctx.circfill(520, 280, 40, "darkgreen")
|
||||||
|
ctx.circfill(480, 300, 40, "darkgreen")
|
||||||
|
ctx.circfill(560, 300, 40, "darkgreen")
|
||||||
|
}
|
||||||
|
|
||||||
|
function drawCloud(ctx: GameContext, x: number, y: number) {
|
||||||
|
ctx.circfill(x, y, 30, "white")
|
||||||
|
ctx.circfill(x + 30, y - 10, 40, "white")
|
||||||
|
ctx.circfill(x + 60, y, 30, "white")
|
||||||
|
ctx.circfill(x + 30, y + 10, 35, "white")
|
||||||
|
}
|
||||||
|
|
@ -3,6 +3,8 @@
|
||||||
|
|
||||||
import { Glob } from "bun"
|
import { Glob } from "bun"
|
||||||
import { watch } from "fs"
|
import { watch } from "fs"
|
||||||
|
import { join } from "path"
|
||||||
|
import { isFile } from "./utils"
|
||||||
import { sendAll } from "./websocket"
|
import { sendAll } from "./websocket"
|
||||||
import { expectDir } from "./utils"
|
import { expectDir } from "./utils"
|
||||||
import { NOSE_SYS_BIN, NOSE_BIN } from "./config"
|
import { NOSE_SYS_BIN, NOSE_BIN } from "./config"
|
||||||
|
|
@ -30,3 +32,23 @@ export async function findCommands(path: string): Promise<string[]> {
|
||||||
|
|
||||||
return list
|
return list
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function commandPath(cmd: string): string | undefined {
|
||||||
|
return [
|
||||||
|
join(NOSE_SYS_BIN, cmd + ".ts"),
|
||||||
|
join(NOSE_SYS_BIN, cmd + ".tsx"),
|
||||||
|
join(NOSE_BIN, cmd + ".ts"),
|
||||||
|
join(NOSE_BIN, cmd + ".tsx")
|
||||||
|
].find((path: string) => isFile(path))
|
||||||
|
}
|
||||||
|
|
||||||
|
export function commandExists(cmd: string): boolean {
|
||||||
|
return commandPath(cmd) !== undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export async function commandSource(name: string): Promise<string> {
|
||||||
|
const path = commandPath(name)
|
||||||
|
if (!path) return ""
|
||||||
|
return Bun.file(path).text()
|
||||||
|
}
|
||||||
|
|
@ -71,6 +71,10 @@
|
||||||
color: var(--c64-dark-blue);
|
color: var(--c64-dark-blue);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
canvas:focus {
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
a {
|
a {
|
||||||
color: var(--cyan);
|
color: var(--cyan);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -148,3 +148,15 @@
|
||||||
#scrollback .output {
|
#scrollback .output {
|
||||||
white-space: pre-wrap;
|
white-space: pre-wrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* games */
|
||||||
|
|
||||||
|
.game {
|
||||||
|
background-color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.game.active {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
}
|
||||||
|
|
@ -21,7 +21,12 @@ export async function dispatchMessage(ws: any, msg: Message) {
|
||||||
|
|
||||||
async function inputMessage(ws: any, msg: Message) {
|
async function inputMessage(ws: any, msg: Message) {
|
||||||
const result = await runCommand(msg.session || "", msg.id || "", msg.data as string, ws)
|
const result = await runCommand(msg.session || "", msg.id || "", msg.data as string, ws)
|
||||||
|
|
||||||
|
if (typeof result.output === "object" && "game" in result.output) {
|
||||||
|
send(ws, { id: msg.id, type: "game:start", data: result.output.game })
|
||||||
|
} else {
|
||||||
send(ws, { id: msg.id, type: "output", data: result })
|
send(ws, { id: msg.id, type: "output", data: result })
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function saveFileMessage(ws: any, msg: Message) {
|
async function saveFileMessage(ws: any, msg: Message) {
|
||||||
|
|
|
||||||
|
|
@ -3,12 +3,18 @@ import type { FC } from "hono/jsx"
|
||||||
export const Layout: FC = async ({ children, title }) => (
|
export const Layout: FC = async ({ children, title }) => (
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<title>{title || "Nose"}</title>
|
<title>{title || "NOSE (Pluto)"}</title>
|
||||||
<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" />
|
||||||
|
|
||||||
<link href="/css/reset.css" rel="stylesheet" />
|
<link href="/css/reset.css" rel="stylesheet" />
|
||||||
<link href="/css/main.css" rel="stylesheet" />
|
<link href="/css/main.css" rel="stylesheet" />
|
||||||
|
|
||||||
|
<script type="importmap" dangerouslySetInnerHTML={{
|
||||||
|
__html: `{ "imports": { "@/": "/" } }`
|
||||||
|
}} />
|
||||||
<script src="/js/main.js" type="module" async></script>
|
<script src="/js/main.js" type="module" async></script>
|
||||||
|
|
||||||
</head>
|
</head>
|
||||||
<body data-mode="tall">
|
<body data-mode="tall">
|
||||||
<main>
|
<main>
|
||||||
|
|
|
||||||
|
|
@ -27,7 +27,7 @@ export function focusHandler(e: MouseEvent) {
|
||||||
const a = target.closest("a")
|
const a = target.closest("a")
|
||||||
|
|
||||||
if (!a) {
|
if (!a) {
|
||||||
if (target.tagName === "INPUT" || target.tagName === "TEXTAREA")
|
if (target.tagName === "INPUT" || target.tagName === "TEXTAREA" || target.tagName === "CANVAS")
|
||||||
return false
|
return false
|
||||||
|
|
||||||
const selection = window.getSelection() || ""
|
const selection = window.getSelection() || ""
|
||||||
|
|
|
||||||
102
app/src/js/game.ts
Normal file
102
app/src/js/game.ts
Normal file
|
|
@ -0,0 +1,102 @@
|
||||||
|
import type { Message, InputState } from "../shared/types.js"
|
||||||
|
import { GameContext } from "../shared/game.js"
|
||||||
|
import { focusInput } from "./focus.js"
|
||||||
|
import { $ } from "./dom.js"
|
||||||
|
import { randomId } from "../shared/utils.js"
|
||||||
|
import { setStatus, addOutput } from "./scrollback.js"
|
||||||
|
import { browserCommands } from "./commands.js"
|
||||||
|
|
||||||
|
const FPS = 30
|
||||||
|
let oldMode = "cinema"
|
||||||
|
let running = false
|
||||||
|
let canvas: HTMLCanvasElement
|
||||||
|
type Game = { init?: () => void, update?: (delta: number, input: InputState) => void, draw?: (ctx: GameContext) => void }
|
||||||
|
|
||||||
|
const pressedStack = new Set<string>()
|
||||||
|
|
||||||
|
let pressed: InputState = {
|
||||||
|
key: "",
|
||||||
|
shift: false,
|
||||||
|
ctrl: false,
|
||||||
|
meta: false,
|
||||||
|
pressed: pressedStack
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function handleGameStart(msg: Message) {
|
||||||
|
const msgId = msg.id as string
|
||||||
|
const name = msg.data as string
|
||||||
|
|
||||||
|
let game
|
||||||
|
try {
|
||||||
|
game = await import(`/command/${name}`)
|
||||||
|
} catch (err: any) {
|
||||||
|
setStatus(msgId, "error")
|
||||||
|
addOutput(msgId, `Error: ${err.message ? err.message : err}`)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const canvasId = randomId()
|
||||||
|
addOutput(msgId, { html: `<canvas id="${canvasId}" class="game active" height="540" width="960" tabindex="0"></canvas>` })
|
||||||
|
|
||||||
|
if (document.body.dataset.mode === "tall") {
|
||||||
|
browserCommands.mode?.()
|
||||||
|
oldMode = "tall"
|
||||||
|
}
|
||||||
|
|
||||||
|
canvas = $(canvasId) as HTMLCanvasElement
|
||||||
|
canvas.focus()
|
||||||
|
setStatus(msgId, "ok")
|
||||||
|
canvas.addEventListener("keydown", handleKeydown)
|
||||||
|
canvas.addEventListener("keyup", handleKeyup)
|
||||||
|
gameLoop(new GameContext(canvas.getContext("2d")!), game)
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleKeydown(e: KeyboardEvent) {
|
||||||
|
pressedStack.add(e.key)
|
||||||
|
e.preventDefault()
|
||||||
|
|
||||||
|
if (e.key === "Escape" || (e.ctrlKey && e.key === "c")) {
|
||||||
|
running = false
|
||||||
|
if (oldMode === "tall") browserCommands.mode?.()
|
||||||
|
canvas.classList.remove("active")
|
||||||
|
canvas.style.height = canvas.height / 2 + "px"
|
||||||
|
canvas.style.width = canvas.width / 2 + "px"
|
||||||
|
focusInput()
|
||||||
|
} else {
|
||||||
|
pressed.key = e.key
|
||||||
|
pressed.ctrl = e.ctrlKey
|
||||||
|
pressed.shift = e.shiftKey
|
||||||
|
pressed.meta = e.metaKey
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleKeyup(e: KeyboardEvent) {
|
||||||
|
pressedStack.delete(e.key)
|
||||||
|
if (pressedStack.size === 0) {
|
||||||
|
pressed.key = ""
|
||||||
|
pressed.ctrl = false
|
||||||
|
pressed.shift = false
|
||||||
|
pressed.meta = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function gameLoop(ctx: GameContext, game: Game) {
|
||||||
|
running = true
|
||||||
|
let last = 0
|
||||||
|
|
||||||
|
if (game.init) game.init()
|
||||||
|
|
||||||
|
function loop(ts: number) {
|
||||||
|
if (!running) return
|
||||||
|
|
||||||
|
const delta = ts - last
|
||||||
|
if (delta >= 1000 / FPS) {
|
||||||
|
if (game.update) game.update(delta, pressed)
|
||||||
|
if (game.draw) game.draw(ctx)
|
||||||
|
last = ts
|
||||||
|
}
|
||||||
|
requestAnimationFrame(loop)
|
||||||
|
}
|
||||||
|
|
||||||
|
requestAnimationFrame(loop)
|
||||||
|
}
|
||||||
|
|
@ -7,6 +7,7 @@ import { send } from "./websocket.js"
|
||||||
import { randomId } from "../shared/utils.js"
|
import { randomId } from "../shared/utils.js"
|
||||||
import { addToHistory } from "./history.js"
|
import { addToHistory } from "./history.js"
|
||||||
import { browserCommands, cacheCommands } from "./commands.js"
|
import { browserCommands, cacheCommands } from "./commands.js"
|
||||||
|
import { handleGameStart } from "./game.js"
|
||||||
|
|
||||||
export function runCommand(input: string) {
|
export function runCommand(input: string) {
|
||||||
if (!input.trim()) return
|
if (!input.trim()) return
|
||||||
|
|
@ -29,7 +30,7 @@ export function runCommand(input: string) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// message received from server
|
// message received from server
|
||||||
export function handleMessage(msg: Message) {
|
export async function handleMessage(msg: Message) {
|
||||||
switch (msg.type) {
|
switch (msg.type) {
|
||||||
case "output":
|
case "output":
|
||||||
handleOutput(msg); break
|
handleOutput(msg); break
|
||||||
|
|
@ -45,6 +46,8 @@ export function handleMessage(msg: Message) {
|
||||||
handleStreamAppend(msg); break
|
handleStreamAppend(msg); break
|
||||||
case "stream:replace":
|
case "stream:replace":
|
||||||
handleStreamReplace(msg); break
|
handleStreamReplace(msg); break
|
||||||
|
case "game:start":
|
||||||
|
await handleGameStart(msg); break
|
||||||
default:
|
default:
|
||||||
console.error("unknown message type", msg)
|
console.error("unknown message type", msg)
|
||||||
}
|
}
|
||||||
|
|
@ -78,3 +81,4 @@ function handleStreamReplace(msg: Message) {
|
||||||
|
|
||||||
function handleStreamEnd(_msg: Message) {
|
function handleStreamEnd(_msg: Message) {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -29,10 +29,10 @@ export function send(msg: Message) {
|
||||||
console.log("-> send", msg)
|
console.log("-> send", msg)
|
||||||
}
|
}
|
||||||
|
|
||||||
function receive(e: MessageEvent) {
|
async function receive(e: MessageEvent) {
|
||||||
const data = JSON.parse(e.data) as Message
|
const data = JSON.parse(e.data) as Message
|
||||||
console.log("<- receive", data)
|
console.log("<- receive", data)
|
||||||
handleMessage(data)
|
await handleMessage(data)
|
||||||
}
|
}
|
||||||
|
|
||||||
// close it... plz don't do this, though
|
// close it... plz don't do this, though
|
||||||
|
|
|
||||||
|
|
@ -9,9 +9,9 @@ import color from "kleur"
|
||||||
import type { Message } from "./shared/types"
|
import type { Message } from "./shared/types"
|
||||||
import { NOSE_ICON, NOSE_BIN, NOSE_WWW } from "./config"
|
import { NOSE_ICON, NOSE_BIN, NOSE_WWW } from "./config"
|
||||||
import { transpile, isFile, tilde } from "./utils"
|
import { transpile, isFile, tilde } from "./utils"
|
||||||
import { apps, serveApp } from "./webapp"
|
import { serveApp } from "./webapp"
|
||||||
import { initDNS } from "./dns"
|
import { initDNS } from "./dns"
|
||||||
import { commands } from "./commands"
|
import { commands, commandSource, commandPath } from "./commands"
|
||||||
import { send, addWebsocket, removeWebsocket, closeWebsockets } from "./websocket"
|
import { send, addWebsocket, removeWebsocket, closeWebsockets } from "./websocket"
|
||||||
|
|
||||||
import { Layout } from "./html/layout"
|
import { Layout } from "./html/layout"
|
||||||
|
|
@ -42,8 +42,8 @@ app.use("*", async (c, next) => {
|
||||||
app.on("GET", ["/js/:path{.+}", "/shared/:path{.+}"], async c => {
|
app.on("GET", ["/js/:path{.+}", "/shared/:path{.+}"], async c => {
|
||||||
const path = "./src/" + c.req.path.replace("..", ".")
|
const path = "./src/" + c.req.path.replace("..", ".")
|
||||||
|
|
||||||
// path must end in .js
|
// path must end in .js or .ts
|
||||||
if (!path.endsWith(".js")) return c.text("File not found", 404)
|
if (!path.endsWith(".js") && !path.endsWith(".ts")) return c.text("File not found", 404)
|
||||||
|
|
||||||
const ts = path.replace(".js", ".ts")
|
const ts = path.replace(".js", ".ts")
|
||||||
if (isFile(ts)) {
|
if (isFile(ts)) {
|
||||||
|
|
@ -76,16 +76,15 @@ app.use("*", async (c, next) => {
|
||||||
return next()
|
return next()
|
||||||
})
|
})
|
||||||
|
|
||||||
app.get("/apps", c => {
|
app.get("/command/:name", async c => {
|
||||||
const url = new URL(c.req.url)
|
const name = c.req.param("name")
|
||||||
const domain = url.hostname
|
const path = commandPath(name)
|
||||||
let port = url.port
|
if (!path) return c.text("Command not found", 404)
|
||||||
port = port && port !== "80" ? `:${port}` : ""
|
return new Response(await transpile(path), {
|
||||||
|
headers: {
|
||||||
return c.html(<>
|
"Content-Type": "text/javascript"
|
||||||
<h1>apps</h1>
|
}
|
||||||
<ul>{apps().map(app => <li><a href={`http://${app}.${domain}${port}`}>{app}</a></li>)}</ul>
|
})
|
||||||
</>)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
app.get("/", c => c.html(<Layout><Terminal /></Layout>))
|
app.get("/", c => c.html(<Layout><Terminal /></Layout>))
|
||||||
|
|
|
||||||
143
app/src/shared/game.ts
Normal file
143
app/src/shared/game.ts
Normal file
|
|
@ -0,0 +1,143 @@
|
||||||
|
|
||||||
|
export class GameContext {
|
||||||
|
constructor(public ctx: CanvasRenderingContext2D) { }
|
||||||
|
|
||||||
|
get width() { return this.ctx.canvas.width }
|
||||||
|
get height() { return this.ctx.canvas.height }
|
||||||
|
|
||||||
|
clear() {
|
||||||
|
this.ctx.clearRect(0, 0, this.ctx.canvas.width, this.ctx.canvas.height)
|
||||||
|
}
|
||||||
|
|
||||||
|
circ(x: number, y: number, r: number, color = "black") {
|
||||||
|
const c = this.ctx
|
||||||
|
c.save()
|
||||||
|
c.beginPath()
|
||||||
|
c.strokeStyle = color
|
||||||
|
c.arc(x, y, r, 0, Math.PI * 2)
|
||||||
|
c.stroke()
|
||||||
|
c.restore()
|
||||||
|
}
|
||||||
|
|
||||||
|
circfill(x: number, y: number, r: number, color = "black") {
|
||||||
|
const c = this.ctx
|
||||||
|
c.save()
|
||||||
|
c.beginPath()
|
||||||
|
c.fillStyle = color
|
||||||
|
c.arc(x, y, r, 0, Math.PI * 2)
|
||||||
|
c.fill()
|
||||||
|
c.restore()
|
||||||
|
}
|
||||||
|
|
||||||
|
line(x0: number, y0: number, x1: number, y1: number, color = "black") {
|
||||||
|
const c = this.ctx
|
||||||
|
c.save()
|
||||||
|
c.beginPath()
|
||||||
|
c.strokeStyle = color
|
||||||
|
c.moveTo(x0, y0)
|
||||||
|
c.lineTo(x1, y1)
|
||||||
|
c.stroke()
|
||||||
|
c.restore()
|
||||||
|
}
|
||||||
|
|
||||||
|
oval(x0: number, y0: number, x1: number, y1: number, color = "black") {
|
||||||
|
const c = this.ctx
|
||||||
|
const w = x1 - x0
|
||||||
|
const h = y1 - y0
|
||||||
|
c.save()
|
||||||
|
c.beginPath()
|
||||||
|
c.strokeStyle = color
|
||||||
|
c.ellipse(x0 + w / 2, y0 + h / 2, Math.abs(w) / 2, Math.abs(h) / 2, 0, 0, Math.PI * 2)
|
||||||
|
c.stroke()
|
||||||
|
c.restore()
|
||||||
|
}
|
||||||
|
|
||||||
|
ovalfill(x0: number, y0: number, x1: number, y1: number, color = "black") {
|
||||||
|
const c = this.ctx
|
||||||
|
const w = x1 - x0
|
||||||
|
const h = y1 - y0
|
||||||
|
c.save()
|
||||||
|
c.beginPath()
|
||||||
|
c.fillStyle = color
|
||||||
|
c.ellipse(x0 + w / 2, y0 + h / 2, Math.abs(w) / 2, Math.abs(h) / 2, 0, 0, Math.PI * 2)
|
||||||
|
c.fill()
|
||||||
|
c.restore()
|
||||||
|
}
|
||||||
|
|
||||||
|
rect(x0: number, y0: number, x1: number, y1: number, color = "black") {
|
||||||
|
const c = this.ctx
|
||||||
|
c.save()
|
||||||
|
c.beginPath()
|
||||||
|
c.strokeStyle = color
|
||||||
|
c.rect(x0, y0, x1 - x0, y1 - y0)
|
||||||
|
c.stroke()
|
||||||
|
c.restore()
|
||||||
|
}
|
||||||
|
|
||||||
|
rectfill(x0: number, y0: number, x1: number, y1: number, color = "black") {
|
||||||
|
const c = this.ctx
|
||||||
|
c.save()
|
||||||
|
this.ctx.fillStyle = color
|
||||||
|
this.ctx.fillRect(x0, y0, x1 - x0, y1 - y0)
|
||||||
|
c.restore()
|
||||||
|
}
|
||||||
|
|
||||||
|
rrect(x: number, y: number, w: number, h: number, r: number, color = "black") {
|
||||||
|
const c = this.ctx
|
||||||
|
c.save()
|
||||||
|
c.beginPath()
|
||||||
|
c.strokeStyle = color
|
||||||
|
this.roundRectPath(x, y, w, h, r)
|
||||||
|
c.stroke()
|
||||||
|
c.restore()
|
||||||
|
}
|
||||||
|
|
||||||
|
rrectfill(x: number, y: number, w: number, h: number, r: number, color = "black") {
|
||||||
|
const c = this.ctx
|
||||||
|
c.save()
|
||||||
|
c.beginPath()
|
||||||
|
c.fillStyle = color
|
||||||
|
this.roundRectPath(x, y, w, h, r)
|
||||||
|
c.fill()
|
||||||
|
c.restore()
|
||||||
|
}
|
||||||
|
|
||||||
|
trianglefill(x0: number, y0: number, x1: number, y1: number, x2: number, y2: number, color = "black") {
|
||||||
|
return this.polygonfill(
|
||||||
|
[
|
||||||
|
[x0, y0],
|
||||||
|
[x1, y1],
|
||||||
|
[x2, y2]
|
||||||
|
],
|
||||||
|
color
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
polygonfill(points: [number, number][], color = "black") {
|
||||||
|
if (points.length < 3) return // need at least a triangle
|
||||||
|
const c = this.ctx
|
||||||
|
c.save()
|
||||||
|
c.beginPath()
|
||||||
|
c.fillStyle = color
|
||||||
|
c.moveTo(points[0]![0], points[0]![1])
|
||||||
|
for (let i = 1; i < points.length; i++) {
|
||||||
|
c.lineTo(points[i]![0], points[i]![1])
|
||||||
|
}
|
||||||
|
c.closePath()
|
||||||
|
c.fill()
|
||||||
|
c.restore()
|
||||||
|
}
|
||||||
|
|
||||||
|
private roundRectPath(x: number, y: number, w: number, h: number, r: number) {
|
||||||
|
const c = this.ctx
|
||||||
|
c.moveTo(x + r, y)
|
||||||
|
c.lineTo(x + w - r, y)
|
||||||
|
c.quadraticCurveTo(x + w, y, x + w, y + r)
|
||||||
|
c.lineTo(x + w, y + h - r)
|
||||||
|
c.quadraticCurveTo(x + w, y + h, x + w - r, y + h)
|
||||||
|
c.lineTo(x + r, y + h)
|
||||||
|
c.quadraticCurveTo(x, y + h, x, y + h - r)
|
||||||
|
c.lineTo(x, y + r)
|
||||||
|
c.quadraticCurveTo(x, y, x + r, y)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -6,11 +6,14 @@ export type Message = {
|
||||||
}
|
}
|
||||||
|
|
||||||
export type MessageType = "error" | "input" | "output" | "commands" | "save-file"
|
export type MessageType = "error" | "input" | "output" | "commands" | "save-file"
|
||||||
|
| "game:start"
|
||||||
| "stream:start" | "stream:end" | "stream:append" | "stream:replace"
|
| "stream:start" | "stream:end" | "stream:append" | "stream:replace"
|
||||||
|
|
||||||
export type CommandOutput = string | string[] | { html: string }
|
export type CommandOutput = string | string[] | { html: string } | { game: string }
|
||||||
|
|
||||||
export type CommandResult = {
|
export type CommandResult = {
|
||||||
status: "ok" | "error"
|
status: "ok" | "error"
|
||||||
output: CommandOutput
|
output: CommandOutput
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type InputState = { key: string, shift: boolean, ctrl: boolean, meta: boolean, pressed: Set<string> }
|
||||||
|
|
|
||||||
|
|
@ -7,3 +7,16 @@ export function countChar(str: string, char: string): number {
|
||||||
export function randomId(): string {
|
export function randomId(): string {
|
||||||
return Math.random().toString(36).slice(7)
|
return Math.random().toString(36).slice(7)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// rng(1, 5) #=> result can be 1 2 3 4 or 5
|
||||||
|
// rng(2) #=> result can be 1 or 2
|
||||||
|
export function rng(min: number, max = 0) {
|
||||||
|
if (max === 0) {
|
||||||
|
max = min
|
||||||
|
min = 1
|
||||||
|
}
|
||||||
|
|
||||||
|
min = Math.ceil(min)
|
||||||
|
max = Math.floor(max)
|
||||||
|
return Math.floor(Math.random() * (max - min + 1)) + min
|
||||||
|
}
|
||||||
|
|
@ -2,11 +2,9 @@
|
||||||
// Runs commands and such on the server.
|
// Runs commands and such on the server.
|
||||||
// This is the "shell" - the "terminal" is the browser UI.
|
// This is the "shell" - the "terminal" is the browser UI.
|
||||||
|
|
||||||
import { join } from "path"
|
|
||||||
import type { CommandResult, CommandOutput } from "./shared/types"
|
import type { CommandResult, CommandOutput } from "./shared/types"
|
||||||
import type { Session } from "./session"
|
import type { Session } from "./session"
|
||||||
import { NOSE_SYS_BIN, NOSE_BIN } from "./config"
|
import { commandExists, commandPath } from "./commands"
|
||||||
import { isFile } from "./utils"
|
|
||||||
import { ALS } from "./session"
|
import { ALS } from "./session"
|
||||||
|
|
||||||
const sessions: Map<string, Session> = new Map()
|
const sessions: Map<string, Session> = new Map()
|
||||||
|
|
@ -34,6 +32,9 @@ export async function runCommand(sessionId: string, taskId: string, input: strin
|
||||||
async function exec(cmd: string, args: string[]): Promise<["ok" | "error", CommandOutput]> {
|
async function exec(cmd: string, args: string[]): Promise<["ok" | "error", CommandOutput]> {
|
||||||
const module = await import(commandPath(cmd) + "?t+" + Date.now())
|
const module = await import(commandPath(cmd) + "?t+" + Date.now())
|
||||||
|
|
||||||
|
if (module?.game)
|
||||||
|
return ["ok", { game: cmd }]
|
||||||
|
|
||||||
if (!module || !module.default)
|
if (!module || !module.default)
|
||||||
return ["error", `${cmd} has no default export`]
|
return ["error", `${cmd} has no default export`]
|
||||||
|
|
||||||
|
|
@ -69,19 +70,6 @@ function getState(sessionId: string, taskId: string, ws?: any): Session {
|
||||||
return state
|
return state
|
||||||
}
|
}
|
||||||
|
|
||||||
function commandPath(cmd: string): string | undefined {
|
|
||||||
return [
|
|
||||||
join(NOSE_SYS_BIN, cmd + ".ts"),
|
|
||||||
join(NOSE_SYS_BIN, cmd + ".tsx"),
|
|
||||||
join(NOSE_BIN, cmd + ".ts"),
|
|
||||||
join(NOSE_BIN, cmd + ".tsx")
|
|
||||||
].find((path: string) => isFile(path))
|
|
||||||
}
|
|
||||||
|
|
||||||
function commandExists(cmd: string): boolean {
|
|
||||||
return commandPath(cmd) !== undefined
|
|
||||||
}
|
|
||||||
|
|
||||||
function errorMessage(error: Error | any): string {
|
function errorMessage(error: Error | any): string {
|
||||||
if (!(error instanceof Error))
|
if (!(error instanceof Error))
|
||||||
return String(error)
|
return String(error)
|
||||||
|
|
|
||||||
|
|
@ -75,14 +75,16 @@ const transpileCache: Record<string, string> = {}
|
||||||
|
|
||||||
// Transpile the frontend *.ts file at `path` to JavaScript.
|
// Transpile the frontend *.ts file at `path` to JavaScript.
|
||||||
export async function transpile(path: string): Promise<string> {
|
export async function transpile(path: string): Promise<string> {
|
||||||
const code = await Bun.file(path).text()
|
|
||||||
|
|
||||||
const { mtime } = await stat(path)
|
const { mtime } = await stat(path)
|
||||||
const key = `${path}?${mtime}`
|
const key = `${path}?${mtime}`
|
||||||
|
|
||||||
let cached = transpileCache[key]
|
let cached = transpileCache[key]
|
||||||
if (!cached) {
|
if (!cached) {
|
||||||
|
const code = await Bun.file(path).text()
|
||||||
cached = transpiler.transformSync(code)
|
cached = transpiler.transformSync(code)
|
||||||
|
cached = cached.replaceAll(/\bjsxDEV_?\w*\(/g, "jsx(")
|
||||||
|
cached = cached.replaceAll(/\bFragment_?\w*,/g, "Fragment,")
|
||||||
|
|
||||||
transpileCache[key] = cached
|
transpileCache[key] = cached
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user