Compare commits
No commits in common. "5afb7023ec0084b521ec05e8d02a4ca401323b6c" and "2c726c5bd2a1846e66ee5ef1f84914dcf0649eed" have entirely different histories.
5afb7023ec
...
2c726c5bd2
|
|
@ -1,86 +0,0 @@
|
||||||
/// <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,8 +3,6 @@
|
||||||
|
|
||||||
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"
|
||||||
|
|
@ -32,23 +30,3 @@ 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,10 +71,6 @@
|
||||||
color: var(--c64-dark-blue);
|
color: var(--c64-dark-blue);
|
||||||
}
|
}
|
||||||
|
|
||||||
canvas:focus {
|
|
||||||
outline: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
a {
|
a {
|
||||||
color: var(--cyan);
|
color: var(--cyan);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -148,15 +148,3 @@
|
||||||
#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,12 +21,7 @@ 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,18 +3,12 @@ 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 (Pluto)"}</title>
|
<title>{title || "Nose"}</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" || target.tagName === "CANVAS")
|
if (target.tagName === "INPUT" || target.tagName === "TEXTAREA")
|
||||||
return false
|
return false
|
||||||
|
|
||||||
const selection = window.getSelection() || ""
|
const selection = window.getSelection() || ""
|
||||||
|
|
|
||||||
|
|
@ -1,102 +0,0 @@
|
||||||
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,7 +7,6 @@ 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
|
||||||
|
|
@ -30,7 +29,7 @@ export function runCommand(input: string) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// message received from server
|
// message received from server
|
||||||
export async function handleMessage(msg: Message) {
|
export function handleMessage(msg: Message) {
|
||||||
switch (msg.type) {
|
switch (msg.type) {
|
||||||
case "output":
|
case "output":
|
||||||
handleOutput(msg); break
|
handleOutput(msg); break
|
||||||
|
|
@ -46,8 +45,6 @@ export async 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)
|
||||||
}
|
}
|
||||||
|
|
@ -81,4 +78,3 @@ 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)
|
||||||
}
|
}
|
||||||
|
|
||||||
async function receive(e: MessageEvent) {
|
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)
|
||||||
await handleMessage(data)
|
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 { serveApp } from "./webapp"
|
import { apps, serveApp } from "./webapp"
|
||||||
import { initDNS } from "./dns"
|
import { initDNS } from "./dns"
|
||||||
import { commands, commandSource, commandPath } from "./commands"
|
import { commands } 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 or .ts
|
// path must end in .js
|
||||||
if (!path.endsWith(".js") && !path.endsWith(".ts")) return c.text("File not found", 404)
|
if (!path.endsWith(".js")) 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,15 +76,16 @@ app.use("*", async (c, next) => {
|
||||||
return next()
|
return next()
|
||||||
})
|
})
|
||||||
|
|
||||||
app.get("/command/:name", async c => {
|
app.get("/apps", c => {
|
||||||
const name = c.req.param("name")
|
const url = new URL(c.req.url)
|
||||||
const path = commandPath(name)
|
const domain = url.hostname
|
||||||
if (!path) return c.text("Command not found", 404)
|
let port = url.port
|
||||||
return new Response(await transpile(path), {
|
port = port && port !== "80" ? `:${port}` : ""
|
||||||
headers: {
|
|
||||||
"Content-Type": "text/javascript"
|
return c.html(<>
|
||||||
}
|
<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>))
|
||||||
|
|
|
||||||
|
|
@ -1,143 +0,0 @@
|
||||||
|
|
||||||
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,14 +6,11 @@ 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 } | { game: string }
|
export type CommandOutput = string | string[] | { html: 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,16 +7,3 @@ 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,9 +2,11 @@
|
||||||
// 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 { commandExists, commandPath } from "./commands"
|
import { NOSE_SYS_BIN, NOSE_BIN } from "./config"
|
||||||
|
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()
|
||||||
|
|
@ -32,9 +34,6 @@ 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`]
|
||||||
|
|
||||||
|
|
@ -70,6 +69,19 @@ 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,16 +75,14 @@ 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