games
This commit is contained in:
parent
a8102a2730
commit
22cdd68184
|
|
@ -148,3 +148,15 @@
|
|||
#scrollback .output {
|
||||
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) {
|
||||
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 })
|
||||
}
|
||||
}
|
||||
|
||||
async function saveFileMessage(ws: any, msg: Message) {
|
||||
|
|
|
|||
|
|
@ -3,12 +3,18 @@ import type { FC } from "hono/jsx"
|
|||
export const Layout: FC = async ({ children, title }) => (
|
||||
<html lang="en">
|
||||
<head>
|
||||
<title>{title || "Nose"}</title>
|
||||
<title>{title || "NOSE (Pluto)"}</title>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
|
||||
<link href="/css/reset.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>
|
||||
|
||||
</head>
|
||||
<body data-mode="tall">
|
||||
<main>
|
||||
|
|
|
|||
|
|
@ -2,11 +2,14 @@
|
|||
// The shell runs on the server and processes input, returning output.
|
||||
|
||||
import type { Message, CommandResult, CommandOutput } from "../shared/types.js"
|
||||
import { Context } from "../shared/types.js"
|
||||
import { addInput, setStatus, addOutput, appendOutput, replaceOutput } from "./scrollback.js"
|
||||
import { send } from "./websocket.js"
|
||||
import { randomId } from "../shared/utils.js"
|
||||
import { addToHistory } from "./history.js"
|
||||
import { focusInput } from "./focus.js"
|
||||
import { browserCommands, cacheCommands } from "./commands.js"
|
||||
import { $ } from "./dom.js"
|
||||
|
||||
export function runCommand(input: string) {
|
||||
if (!input.trim()) return
|
||||
|
|
@ -29,7 +32,7 @@ export function runCommand(input: string) {
|
|||
}
|
||||
|
||||
// message received from server
|
||||
export function handleMessage(msg: Message) {
|
||||
export async function handleMessage(msg: Message) {
|
||||
switch (msg.type) {
|
||||
case "output":
|
||||
handleOutput(msg); break
|
||||
|
|
@ -45,6 +48,8 @@ export function handleMessage(msg: Message) {
|
|||
handleStreamAppend(msg); break
|
||||
case "stream:replace":
|
||||
handleStreamReplace(msg); break
|
||||
case "game:start":
|
||||
await handleGameStart(msg); break
|
||||
default:
|
||||
console.error("unknown message type", msg)
|
||||
}
|
||||
|
|
@ -78,3 +83,53 @@ function handleStreamReplace(msg: Message) {
|
|||
|
||||
function handleStreamEnd(_msg: Message) {
|
||||
}
|
||||
|
||||
let oldMode = "cinema"
|
||||
|
||||
async function handleGameStart(msg: Message) {
|
||||
const msgId = msg.id as string
|
||||
const name = msg.data as string
|
||||
const game = await import(`/command/${name}`)
|
||||
const id = randomId()
|
||||
let stopGame = false
|
||||
|
||||
addOutput(msgId, { html: `<canvas id="${id}" class="game active" height="540" width="960" tabindex="0"></canvas>` })
|
||||
|
||||
if (document.body.dataset.mode === "tall") {
|
||||
browserCommands.mode?.()
|
||||
oldMode = "tall"
|
||||
}
|
||||
|
||||
setStatus(msgId, "ok")
|
||||
|
||||
const canvas = $(id) as HTMLCanvasElement
|
||||
const ctx = new Context(canvas.getContext("2d")!)
|
||||
canvas.focus()
|
||||
canvas.addEventListener("keydown", e => {
|
||||
e.preventDefault()
|
||||
|
||||
if (e.key === "Escape" || (e.ctrlKey && e.key === "c")) {
|
||||
stopGame = true
|
||||
if (oldMode === "tall") browserCommands.mode?.()
|
||||
canvas.classList.remove("active")
|
||||
canvas.style.height = canvas.height / 2 + "px"
|
||||
canvas.style.width = canvas.width / 2 + "px"
|
||||
focusInput()
|
||||
}
|
||||
})
|
||||
|
||||
let last = 0
|
||||
function loop(ts: number) {
|
||||
if (stopGame) return
|
||||
|
||||
const delta = ts - last
|
||||
if (delta >= 1000 / 30) {
|
||||
if (game.update) game.update(delta)
|
||||
if (game.draw) game.draw(ctx)
|
||||
last = ts
|
||||
}
|
||||
requestAnimationFrame(loop)
|
||||
}
|
||||
requestAnimationFrame(loop)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -29,10 +29,10 @@ export function send(msg: Message) {
|
|||
console.log("-> send", msg)
|
||||
}
|
||||
|
||||
function receive(e: MessageEvent) {
|
||||
async function receive(e: MessageEvent) {
|
||||
const data = JSON.parse(e.data) as Message
|
||||
console.log("<- receive", data)
|
||||
handleMessage(data)
|
||||
await handleMessage(data)
|
||||
}
|
||||
|
||||
// close it... plz don't do this, though
|
||||
|
|
|
|||
|
|
@ -6,11 +6,31 @@ export type Message = {
|
|||
}
|
||||
|
||||
export type MessageType = "error" | "input" | "output" | "commands" | "save-file"
|
||||
| "game:start"
|
||||
| "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 = {
|
||||
status: "ok" | "error"
|
||||
output: CommandOutput
|
||||
}
|
||||
|
||||
export class Context {
|
||||
height = 540
|
||||
width = 960
|
||||
|
||||
constructor(public ctx: CanvasRenderingContext2D) { }
|
||||
|
||||
circ(x: number, y: number, r: number, color = "black") {
|
||||
const ctx = this.ctx
|
||||
ctx.beginPath()
|
||||
ctx.strokeStyle = color
|
||||
ctx.arc(x, y, r, 0, Math.PI * 2)
|
||||
ctx.stroke()
|
||||
}
|
||||
|
||||
clear() {
|
||||
this.ctx.clearRect(0, 0, this.width, this.height)
|
||||
}
|
||||
}
|
||||
|
|
@ -32,6 +32,9 @@ export async function runCommand(sessionId: string, taskId: string, input: strin
|
|||
async function exec(cmd: string, args: string[]): Promise<["ok" | "error", CommandOutput]> {
|
||||
const module = await import(commandPath(cmd) + "?t+" + Date.now())
|
||||
|
||||
if (module?.game)
|
||||
return ["ok", { game: cmd }]
|
||||
|
||||
if (!module || !module.default)
|
||||
return ["error", `${cmd} has no default export`]
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user