Merge branch 'main' into dev

This commit is contained in:
dylan 2024-04-02 08:43:26 -07:00
commit 2cf11a3f36
13 changed files with 238 additions and 24 deletions

1
.gitignore vendored
View File

@ -1,3 +1,4 @@
node_modules node_modules
src/server/public/dist src/server/public/dist
src/server/shrinko8
.env .env

101
src/client/GamePage.tsx Normal file
View File

@ -0,0 +1,101 @@
import { useNavigate, useParams } from "react-router-dom"
import { Pico8Console } from "./pico8-client/Pico8Console";
import { useEffect, useState } from "react";
import { DbRelease } from "../server/dbal/dbal";
import { css } from "@emotion/css";
type Info = {
release: DbRelease | null;
versions: string[];
}
export const GamePage = () => {
const {author, slug, version} = useParams();
const navigate = useNavigate();
const [info, setInfo] = useState<Info | null>(null);
useEffect(() => {
const fetchInfo = async () => {
let url = `/api/release?author=${author}&slug=${slug}`;
if (version) {
url += `&version=${version.slice(1)}`;
}
const information = await fetch(url);
const json = await information.json();
setInfo(json);
}
fetchInfo();
}, [setInfo, author, slug, version]);
if (!info) {
return (
<div className={`
min-height: 100vh;
`}>
LOADING...
</div>
)
}
if (!info.release) {
return (
<div className={`
min-height: 100vh;
`}>
NOT FOUND
</div>
)
}
return (
<div className={css`
min-height: 100vh;
background-color: hsl(230, 10%, 10%);
color: white;
`}>
<div className={css`
margin: auto;
width: max-content;
max-inline-size: 66ch;
padding: 1.5em;
display: flex;
flex-direction: column;
gap: 1em;
`}>
<div>
<h1>{info.release.manifest.title ?? slug!.split("-").map(word => word[0].toUpperCase()+word.slice(1)).join(" ")}</h1>
<h2>by {info.release.author}</h2>
</div>
<div className={css`
width: 512px;
max-width: 100%;
margin: auto;
`}>
<div className={css`
border: 2px solid transparent;
&:focus-within {
border: 2px solid limegreen;
}
`}>
<Pico8Console carts={info.release.carts} />
</div>
<div className={css`
display: flex;
justify-content: end;
`}>
Version: <select defaultValue={info.release.version} onChange={(ev) => navigate(`/u/${author}/${slug}/v${ev.target.value}`)}>
{
[...info.versions].reverse().map(v => (
<option key={v} value={v}>{v}</option>
))
}
</select>
</div>
</div>
{/* <div>
<p>This is a paragraph about this game. It is a cool game. And a cool website to play it on. It automagically connects from GitHub.</p>
</div> */}
</div>
</div>
)
}

3
src/client/HomePage.tsx Normal file
View File

@ -0,0 +1,3 @@
export const HomePage = () => {
return <div>Welcome to Picobook!</div>
}

View File

@ -1,15 +1,17 @@
import { css } from "@emotion/css"; import { css } from "@emotion/css";
import { Pico8Console } from "./pico8-client/Pico8Console"; import { Pico8Console } from "./pico8-client/Pico8Console";
import testcarts from "./testcarts"; import testcarts from "./testcarts";
import { Routing } from "./routing";
const App = (props: {}) => { const App = (props: {}) => {
return ( return (
<div className={css` <Routing/>
min-height: 100vh; // <div className={css`
`}> // min-height: 100vh;
<h1>Picobook</h1> // `}>
<Pico8Console carts={testcarts.carts} /> // <h1>Picobook</h1>
</div> // <Pico8Console carts={testcarts.carts} />
// </div>
); );
}; };

View File

@ -8,16 +8,27 @@ type Pico8ConsoleImperatives = {
export const Pico8Console = forwardRef((props: { carts: PicoCart[] }, forwardedRef: ForwardedRef<Pico8ConsoleImperatives>) => { export const Pico8Console = forwardRef((props: { carts: PicoCart[] }, forwardedRef: ForwardedRef<Pico8ConsoleImperatives>) => {
const {carts} = props; const {carts} = props;
const [playing, setPlaying] = useState(false);
const ref = useRef<HTMLDivElement>(null); const ref = useRef<HTMLDivElement>(null);
const [handle, setHandle] = useState<PicoPlayerHandle | null>(null); const [handle, setHandle] = useState<PicoPlayerHandle | null>(null);
const attachConsole = useCallback(async () => { const attachConsole = useCallback(async () => {
const picoConsole = await makePicoConsole({ const picoConsole = await makePicoConsole({
carts, carts,
}); });
picoConsole.canvas.tabIndex=0;
if (ref.current) { if (ref.current) {
ref.current.appendChild(picoConsole.canvas); ref.current.appendChild(picoConsole.canvas);
picoConsole.canvas.focus();
} }
setHandle(picoConsole); setHandle(picoConsole);
picoConsole.canvas.addEventListener('keydown',(event) => {
if (["ArrowUp", "ArrowDown", "ArrowLeft", "ArrowRight"].includes(event.key)) {
event.preventDefault();
}
}, {passive: false});
picoConsole.canvas.addEventListener('click', () => {
picoConsole.canvas.focus();
})
}, [carts]); }, [carts]);
useImperativeHandle(forwardedRef, () => ({ useImperativeHandle(forwardedRef, () => ({
getPicoConsoleHandle() { getPicoConsoleHandle() {
@ -25,8 +36,37 @@ export const Pico8Console = forwardRef((props: { carts: PicoCart[] }, forwardedR
} }
}), [handle]); }), [handle]);
useEffect(() => { useEffect(() => {
attachConsole(); if (playing) {
}, [attachConsole]); attachConsole();
return () => {
if (ref.current) {
ref.current.innerHTML = "";
}
}
}
}, [playing, attachConsole]);
if (!playing) {
return (
<div
ref={ref}
className={css`
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
aspect-ratio: 1;
background-color: black;
color: white;
cursor: pointer;
`}
tabIndex={0}
onClick={() => {setPlaying(true)}}
>
Play!
</div>
)
}
return ( return (
<div ref={ref} className={css` <div ref={ref} className={css`
width: 100%; width: 100%;

View File

@ -18435,6 +18435,7 @@ use chrome, FireFox or Internet Explorer 11`);
var require_veryRawRenderCart = __commonJS((exports, module) => { var require_veryRawRenderCart = __commonJS((exports, module) => {
var __dirname = "/home/dylan/repos/picobook/src/client/pico8-client"; var __dirname = "/home/dylan/repos/picobook/src/client/pico8-client";
window.P8 = function(Module, cartNames, cartDatas) { window.P8 = function(Module, cartNames, cartDatas) {
const codo_textarea_el = Module.codo_textarea || document.getElementById("codo_textarea");
let p8_touch_detected; let p8_touch_detected;
let p8_dropped_cart; let p8_dropped_cart;
let p8_dropped_cart_name; let p8_dropped_cart_name;
@ -19390,7 +19391,7 @@ var require_veryRawRenderCart = __commonJS((exports, module) => {
}, function() { }, function() {
if (typeof codo_key_buffer === "undefined") if (typeof codo_key_buffer === "undefined")
codo_key_buffer = []; codo_key_buffer = [];
document.addEventListener("keydown", function(e) { Module["canvas"].addEventListener("keydown", function(e) {
var val = -1; var val = -1;
if (e.key.length == 1) { if (e.key.length == 1) {
val = e.key.charCodeAt(0); val = e.key.charCodeAt(0);
@ -19406,7 +19407,7 @@ var require_veryRawRenderCart = __commonJS((exports, module) => {
if (val == -1) { if (val == -1) {
} }
} }
var el2 = document.getElementById("codo_textarea"); var el2 = codo_textarea_el;
codo_key_buffer.push(val); codo_key_buffer.push(val);
}); });
}, function() { }, function() {
@ -19515,7 +19516,7 @@ var require_veryRawRenderCart = __commonJS((exports, module) => {
}, function() { }, function() {
if (document.hidden) if (document.hidden)
return 0; return 0;
el = typeof codo_textarea === "undefined" ? document.getElementById("codo_textarea") : codo_textarea; el = typeof codo_textarea === "undefined" ? codo_textarea_el : codo_textarea;
if (el && el == document.activeElement) if (el && el == document.activeElement)
return 1; return 1;
el = document.activeElement; el = document.activeElement;
@ -19529,13 +19530,13 @@ var require_veryRawRenderCart = __commonJS((exports, module) => {
} }
return 0; return 0;
}, function() { }, function() {
el = typeof codo_textarea === "undefined" ? document.getElementById("codo_textarea") : codo_textarea; el = typeof codo_textarea === "undefined" ? codo_textarea_el : codo_textarea;
if (el && el.style.display != "none") { if (el && el.style.display != "none") {
el.focus(); el.focus();
el.select(); el.select();
} }
}, function() { }, function() {
el = typeof codo_textarea === "undefined" ? document.getElementById("codo_textarea") : codo_textarea; el = typeof codo_textarea === "undefined" ? codo_textarea_el : codo_textarea;
if (el && el.style.display != "none") { if (el && el.style.display != "none") {
el.select(); el.select();
} }
@ -19550,7 +19551,7 @@ var require_veryRawRenderCart = __commonJS((exports, module) => {
}, function() { }, function() {
Module["canvas"].exitPointerLock(); Module["canvas"].exitPointerLock();
}, function() { }, function() {
el = typeof codo_textarea === "undefined" ? document.getElementById("codo_textarea") : codo_textarea; el = typeof codo_textarea === "undefined" ? codo_textarea_el : codo_textarea;
if (el) { if (el) {
} }
}, function() { }, function() {
@ -19558,21 +19559,21 @@ var require_veryRawRenderCart = __commonJS((exports, module) => {
}, function($0, $1) { }, function($0, $1) {
_codo_str_out = Module.UTF8ToString($0, $1); _codo_str_out = Module.UTF8ToString($0, $1);
}, function() { }, function() {
el = typeof codo_textarea === "undefined" ? document.getElementById("codo_textarea") : codo_textarea; el = typeof codo_textarea === "undefined" ? codo_textarea_el : codo_textarea;
if (el) { if (el) {
el.value = _codo_str_out; el.value = _codo_str_out;
return 0; return 0;
} else } else
return 1; return 1;
}, function() { }, function() {
el = typeof codo_textarea === "undefined" ? document.getElementById("codo_textarea") : codo_textarea; el = typeof codo_textarea === "undefined" ? codo_textarea_el : codo_textarea;
if (el && el.style.display == "none" && (typeof p8_touch_detected === "undefined" || !p8_touch_detected)) { if (el && el.style.display == "none" && (typeof p8_touch_detected === "undefined" || !p8_touch_detected)) {
el.style.display = ""; el.style.display = "";
el.focus(); el.focus();
el.select(); el.select();
} }
}, function() { }, function() {
el = typeof codo_textarea === "undefined" ? document.getElementById("codo_textarea") : codo_textarea; el = typeof codo_textarea === "undefined" ? codo_textarea_el : codo_textarea;
if (el && el.style.display != "none" && el.value != "") { if (el && el.style.display != "none" && el.value != "") {
_codo_text_value = el.value; _codo_text_value = el.value;
return 1; return 1;

View File

@ -3,7 +3,7 @@ import "./build/veryRawRenderCart.js";
export type PicoBool = 0 | 1; export type PicoBool = 0 | 1;
export type RenderCart = (Module: {canvas: HTMLCanvasElement}, cartNames: string[], cartDatas: number[][], audioContext: AudioContext) => { export type RenderCart = (Module: {canvas: HTMLCanvasElement, codo_textarea?: HTMLTextAreaElement}, cartNames: string[], cartDatas: number[][], audioContext: AudioContext) => {
p8_touch_detected?: PicoBool; p8_touch_detected?: PicoBool;
p8_dropped_cart?: string; p8_dropped_cart?: string;
p8_dropped_cart_name?: string; p8_dropped_cart_name?: string;

View File

@ -80,12 +80,18 @@ const getRom = async (cart: PicoCart) => {
export const makePicoConsole = async (props: { export const makePicoConsole = async (props: {
canvas?: HTMLCanvasElement; canvas?: HTMLCanvasElement;
codoTextarea?: HTMLTextAreaElement;
audioContext?: AudioContext; audioContext?: AudioContext;
carts: PicoCart[]; carts: PicoCart[];
}): Promise<PicoPlayerHandle> => { }): Promise<PicoPlayerHandle> => {
const {carts, canvas = document.createElement("canvas"), audioContext = new AudioContext()} = props; const {carts, canvas = document.createElement("canvas"), codoTextarea = document.createElement("textarea"), audioContext = new AudioContext()} = props;
canvas.style.imageRendering = "pixelated"; canvas.style.imageRendering = "pixelated";
const Module = {canvas}; codoTextarea.style.display="none";
codoTextarea.style.position="fixed";
codoTextarea.style.left="-9999px";
codoTextarea.style.height="0px";
codoTextarea.style.overflow="hidden";
const Module = {canvas, keyboardListeningElement: canvas};
const cartsDatas = await Promise.all(carts.map(cart => getRom(cart))); const cartsDatas = await Promise.all(carts.map(cart => getRom(cart)));
const handle = rawRenderCart(Module, carts.map(cart => cart.name), cartsDatas, audioContext); const handle = rawRenderCart(Module, carts.map(cart => cart.name), cartsDatas, audioContext);
handle.pico8_state = {}; handle.pico8_state = {};

File diff suppressed because one or more lines are too long

37
src/client/routing.tsx Normal file
View File

@ -0,0 +1,37 @@
import { Outlet, RouterProvider, ScrollRestoration, createBrowserRouter, redirect } from "react-router-dom"
import { HomePage } from "./HomePage";
import { GamePage } from "./GamePage";
const RouteRoot = () => {
return <>
{/* <Nav> */}
<ScrollRestoration />
<Outlet/>
{/* </Nav> */}
</>
}
const router = createBrowserRouter([
{
element: <RouteRoot/>,
children: [
{
path: "/",
element: <HomePage/>,
// loader: () => {
// return redirect("/megachat");
// }
},
{
path: "/u/:author/:slug",
element: <GamePage/>,
},
{
path: "/u/:author/:slug/:version",
element: <GamePage/>,
},
],
},
]);
export const Routing = () => <RouterProvider router={router} />;

View File

@ -24,7 +24,13 @@ server.register(fastifyStatic, {
routeList.forEach(firRoute => { routeList.forEach(firRoute => {
server.route(route(firRoute)); server.route(route(firRoute));
}) });
server.setNotFoundHandler((req, res) => {
if (!req.url.startsWith("/api")) {
res.sendFile('index.html');
}
});
// Run the server! // Run the server!
try { try {

View File

@ -1,7 +1,7 @@
<!DOCTYPE html> <!DOCTYPE html>
<html> <html>
<head> <head>
<script src="dist/index.js" type="module"></script> <script src="/dist/index.js" type="module"></script>
<style> <style>
:root { :root {
--measure: 64ch; --measure: 64ch;

View File

@ -1120,7 +1120,22 @@ canvas{
<!-- Add content below the cart here --> <!-- Add content below the cart here -->
<div>
Content here<br/>
Content here<br/>
Content here<br/>
Content here<br/>
Content here<br/>
Content here<br/>
Content here<br/>
Content here<br/>
Content here<br/>
Content here<br/>
Content here<br/>
Content here<br/>
Content here<br/>
<input type="text"/>
</div>
</div> <!-- body_0 --> </div> <!-- body_0 -->