games
This commit is contained in:
parent
a8102a2730
commit
22cdd68184
|
|
@ -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)
|
||||||
send(ws, { id: msg.id, type: "output", data: result })
|
|
||||||
|
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) {
|
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>
|
||||||
|
|
|
||||||
|
|
@ -2,11 +2,14 @@
|
||||||
// The shell runs on the server and processes input, returning output.
|
// The shell runs on the server and processes input, returning output.
|
||||||
|
|
||||||
import type { Message, CommandResult, CommandOutput } from "../shared/types.js"
|
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 { addInput, setStatus, addOutput, appendOutput, replaceOutput } from "./scrollback.js"
|
||||||
import { send } from "./websocket.js"
|
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 { focusInput } from "./focus.js"
|
||||||
import { browserCommands, cacheCommands } from "./commands.js"
|
import { browserCommands, cacheCommands } from "./commands.js"
|
||||||
|
import { $ } from "./dom.js"
|
||||||
|
|
||||||
export function runCommand(input: string) {
|
export function runCommand(input: string) {
|
||||||
if (!input.trim()) return
|
if (!input.trim()) return
|
||||||
|
|
@ -29,7 +32,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 +48,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 +83,53 @@ function handleStreamReplace(msg: Message) {
|
||||||
|
|
||||||
function handleStreamEnd(_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)
|
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
|
||||||
|
|
|
||||||
|
|
@ -6,11 +6,31 @@ 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 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]> {
|
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`]
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user