Start showcase pages (#657)

This commit is contained in:
Corwin 2024-04-30 20:52:20 +01:00 committed by GitHub
commit 737a1582dd
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
50 changed files with 531 additions and 118 deletions

View file

@ -98,18 +98,19 @@ setup-cargo-wasm:
build-website-backtrace:
(cd website/backtrace && wasm-pack build --target web)
rm -rf website/agb/src/app/vendor/backtrace
mkdir -p website/agb/src/app/vendor
cp website/backtrace/pkg website/agb/src/app/vendor/backtrace -r
rm -rf website/agb/src/vendor/backtrace
mkdir -p website/agb/src/vendor
cp website/backtrace/pkg website/agb/src/vendor/backtrace -r
build-mgba-wasm:
rm -rf website/agb/src/app/mgba/vendor
mkdir website/agb/src/app/mgba/vendor
{{podman_command}} build --file website/mgba-wasm/BuildMgbaWasm --output=website/agb/src/app/mgba/vendor .
rm -rf website/agb/src/components/mgba/vendor
mkdir website/agb/src/components/mgba/vendor
{{podman_command}} build --file website/mgba-wasm/BuildMgbaWasm --output=website/agb/src/components/mgba/vendor .
build-combo-rom-site:
just _build-rom "examples/combo" "AGBGAMES"
gzip -9 -c examples/target/examples/combo.gba > website/agb/src/app/combo.gba.gz
mkdir -p website/agb/src/roms
gzip -9 -c examples/target/examples/combo.gba > website/agb/src/roms/combo.gba.gz
setup-app-build: build-mgba-wasm build-combo-rom-site build-website-backtrace

View file

@ -1,6 +1,6 @@
"use client";
import { ContentBlock } from "../contentBlock";
import { ContentBlock } from "../../components/contentBlock";
import { useState } from "react";
import { styled } from "styled-components";
@ -69,44 +69,48 @@ export default function ColourPicker() {
}
return (
<ContentBlock>
<h1>agbrs colour converter</h1>
<PickerWrapper>
<PickerColumn>
<h2>Regular RGB8</h2>
<ColourInput
type="color"
value={hexColour}
onChange={(evt) => setHexColour(evt.target.value)}
/>
<Input
type="text"
value={hexColour}
onChange={(evt) => setHexColour(evt.target.value)}
/>
</PickerColumn>
<PickerColumn>
<h2>GBA RGB5</h2>
<ColourInput
type="color"
value={gbaHexColour}
onChange={(evt) => setGbaHexColour(evt.target.value)}
/>
<Input
type="text"
value={gbaHexColour}
onChange={(evt) => setGbaHexColour(evt.target.value)}
/>
<Input
type="text"
value={gbaU16}
onChange={(evt) =>
setColour(fromRgb15(parseInt(evt.target.value, 16)))
}
/>
</PickerColumn>
</PickerWrapper>
</ContentBlock>
<>
<ContentBlock color="#AAAFFF">
<h1>agbrs colour converter</h1>
</ContentBlock>
<ContentBlock>
<PickerWrapper>
<PickerColumn>
<h2>Regular RGB8</h2>
<ColourInput
type="color"
value={hexColour}
onChange={(evt) => setHexColour(evt.target.value)}
/>
<Input
type="text"
value={hexColour}
onChange={(evt) => setHexColour(evt.target.value)}
/>
</PickerColumn>
<PickerColumn>
<h2>GBA RGB5</h2>
<ColourInput
type="color"
value={gbaHexColour}
onChange={(evt) => setGbaHexColour(evt.target.value)}
/>
<Input
type="text"
value={gbaHexColour}
onChange={(evt) => setGbaHexColour(evt.target.value)}
/>
<Input
type="text"
value={gbaU16}
onChange={(evt) =>
setColour(fromRgb15(parseInt(evt.target.value, 16)))
}
/>
</PickerColumn>
</PickerWrapper>
</ContentBlock>
</>
);
}

View file

@ -2,7 +2,7 @@
import { useEffect, useState } from "react";
import { ContentBlock } from "../contentBlock";
import { ContentBlock } from "../../components/contentBlock";
import { GameDeveloperSummary } from "./gameDeveloperSummary";
import { styled } from "styled-components";
import { Debug } from "./debug";
@ -14,30 +14,39 @@ export function BacktracePage() {
}, []);
return (
<ContentBlock>
<h1>agbrs crash backtrace viewer</h1>
<p>
You likely got here from the link / QR code that was displayed when a
game you were playing crashed. This is the default crash page for games
made using the agb library.
</p>
<p>
The creator of the game is <em>very</em> likely interested in the code
below <em>along with</em> a description of what you were doing at the
time.{" "}
<strong>Send these to the creator of the game you are playing.</strong>
</p>
<BacktraceCopyDisplay backtrace={backtrace} setBacktrace={setBacktrace} />
<p>
<em>
The owners of this website are not necessarily the creators of the
game you are playing.
</em>
</p>
<h2>Backtrace</h2>
{backtrace && <Debug encodedBacktrace={backtrace} />}
<GameDeveloperSummary />
</ContentBlock>
<>
<ContentBlock color="#AAAFFF">
<h1>agbrs crash backtrace viewer</h1>
</ContentBlock>
<ContentBlock>
<p>
You likely got here from the link / QR code that was displayed when a
game you were playing crashed. This is the default crash page for
games made using the agb library.
</p>
<p>
The creator of the game is <em>very</em> likely interested in the code
below <em>along with</em> a description of what you were doing at the
time.{" "}
<strong>
Send these to the creator of the game you are playing.
</strong>
</p>
<BacktraceCopyDisplay
backtrace={backtrace}
setBacktrace={setBacktrace}
/>
<p>
<em>
The owners of this website are not necessarily the creators of the
game you are playing.
</em>
</p>
<h2>Backtrace</h2>
{backtrace && <Debug encodedBacktrace={backtrace} />}
<GameDeveloperSummary />
</ContentBlock>
</>
);
}

View file

@ -1,5 +1,9 @@
import { styled } from "styled-components";
import { AddressInfo, AgbDebug, useAgbDebug } from "../useAgbDebug.hook";
import {
AddressInfo,
AgbDebug,
useAgbDebug,
} from "../../hooks/useAgbDebug.hook";
import { ReactNode, useMemo, useState } from "react";
const BacktraceListWrapper = styled.div`

View file

@ -1,6 +1,6 @@
import type { Metadata } from "next";
import "./globalStyles.css";
import StyledComponentsRegistry from "./registry";
import StyledComponentsRegistry, { BodyPixelRatio } from "./registry";
export const metadata: Metadata = {
title: "agb - a rust framework for making Game Boy Advance games",
@ -13,9 +13,9 @@ export default function RootLayout({
}>) {
return (
<html lang="en">
<body>
<StyledComponentsRegistry>{children}</StyledComponentsRegistry>
</body>
<StyledComponentsRegistry>
<BodyPixelRatio>{children}</BodyPixelRatio>
</StyledComponentsRegistry>
</html>
);
}

View file

Before

Width:  |  Height:  |  Size: 181 B

After

Width:  |  Height:  |  Size: 181 B

View file

@ -1,35 +1,18 @@
"use client";
import styled from "styled-components";
import { CenteredBlock, ContentBlock } from "./contentBlock";
import MgbaWrapper from "./mgba/mgbaWrapper";
import { CenteredBlock, ContentBlock } from "../components/contentBlock";
import MgbaWrapper from "../components/mgba/mgbaWrapper";
import Image from "next/image";
import left from "./gba-parts/left.png";
import right from "./gba-parts/right.png";
import { MobileController } from "./mobileController";
import left from "./left.png";
import right from "./right.png";
import { MobileController } from "../components/mobileController/mobileController";
import { useMemo, useRef, useState } from "react";
import { GbaKey } from "./mgba/bindings";
import { useClientValue } from "./useClientValue.hook";
import { MgbaHandle } from "./mgba/mgba";
const ExternalLink = styled.a`
text-decoration: none;
color: black;
background-color: #fad288;
border: solid #fad288 2px;
border-radius: 5px;
padding: 5px 10px;
&:hover {
border: solid black 2px;
}
`;
const HelpLinks = styled.div`
display: flex;
justify-content: space-around;
`;
import { GbaKey } from "../components/mgba/bindings";
import { useClientValue } from "../hooks/useClientValue.hook";
import { MgbaHandle } from "../components/mgba/mgba";
import { ExternalLink, ExternalLinkBlock } from "@/components/externalLink";
const GameDisplay = styled.div`
height: min(calc(100vw / 1.5), min(90vh, 480px));
@ -78,7 +61,7 @@ function shouldStartPlaying(isTouchScreen: boolean | undefined) {
return !isTouchScreen;
}
const COMBO_GAME = new URL("combo.gba.gz", import.meta.url);
const COMBO_GAME = new URL("../roms/combo.gba.gz", import.meta.url);
function MgbaWithControllerSides() {
const mgba = useRef<MgbaHandle>(null);
@ -136,14 +119,14 @@ function MgbaWithControllerSides() {
export default function Home() {
return (
<>
<ContentBlock>
<ContentBlock color="#AAAFFF">
<h1>agb - a rust framework for making Game Boy Advance games</h1>
</ContentBlock>
<ContentBlock uncentered>
<MgbaWithControllerSides />
</ContentBlock>
<ContentBlock color="#f5755e">
<HelpLinks>
<ContentBlock color="#256256">
<ExternalLinkBlock>
<ExternalLink href="https://github.com/agbrs/agb">
GitHub
</ExternalLink>
@ -151,7 +134,8 @@ export default function Home() {
<ExternalLink href="https://docs.rs/agb/latest/agb/">
Docs
</ExternalLink>
</HelpLinks>
<ExternalLink href="./showcase">Showcase</ExternalLink>
</ExternalLinkBlock>
</ContentBlock>
</>
);

View file

@ -1,8 +1,8 @@
"use client";
import React, { useState } from "react";
import React, { useEffect, useState } from "react";
import { useServerInsertedHTML } from "next/navigation";
import { ServerStyleSheet, StyleSheetManager } from "styled-components";
import styled, { ServerStyleSheet, StyleSheetManager } from "styled-components";
export default function StyledComponentsRegistry({
children,
@ -27,3 +27,18 @@ export default function StyledComponentsRegistry({
</StyleSheetManager>
);
}
const BodyWithPixelRatio = styled.body<{
$pixel: number;
}>`
--device-pixel: calc(1px / ${(props) => props.$pixel});
`;
export function BodyPixelRatio({ children }: { children: React.ReactNode }) {
const [pixel, setPixel] = useState(1);
useEffect(() => {
setPixel(window.devicePixelRatio);
}, []);
return <BodyWithPixelRatio $pixel={pixel}>{children}</BodyWithPixelRatio>;
}

View file

Before

Width:  |  Height:  |  Size: 206 B

After

Width:  |  Height:  |  Size: 206 B

View file

@ -0,0 +1,81 @@
import { slugify } from "@/sluggify";
import { Games, ShowcaseGame } from "../games";
import { ContentBlock } from "@/components/contentBlock";
import { ExternalLink, ExternalLinkBlock } from "@/components/externalLink";
import Link from "next/link";
import {
BackToShowcaseWrapper,
DescriptionAndScreenshots,
Description,
Screenshots,
} from "./styles";
export async function generateStaticParams() {
return Games.map((game) => ({
game: slugify(game.name),
}));
}
function getGame(slug: string) {
const game = Games.find((game) => slugify(game.name) === slug);
if (!game) {
throw new Error("Not valid game name, this should never happen");
}
return game;
}
export function generateMetadata({ params }: { params: { game: string } }) {
const game = getGame(params.game);
return { title: game.name };
}
export default function Page({ params }: { params: { game: string } }) {
const game = getGame(params.game);
return <Display game={game} />;
}
function DeveloperNames({ names }: { names: string[] }) {
if (names.length === 0) {
throw new Error("You must specify developer names");
}
if (names.length === 1) {
return names[0];
}
if (names.length === 2) {
return names.join(" and ");
}
const first = names.slice(0, -1);
return first.join(", ") + `, and ${names[names.length - 1]}`;
}
function Display({ game }: { game: ShowcaseGame }) {
return (
<>
<ContentBlock color="#AAAFFF">
<BackToShowcaseWrapper>
<Link href={`../showcase#${slugify(game.name)}`}>
<strong>&lt;</strong> Back to showcase
</Link>
</BackToShowcaseWrapper>
<h1>{game.name}</h1>
<div>
By: <DeveloperNames names={game.developers} />
</div>
</ContentBlock>
<ContentBlock>
<DescriptionAndScreenshots>
<Description>{game.description}</Description>
<Screenshots screenshots={game.screenshots} />
</DescriptionAndScreenshots>
</ContentBlock>
<ContentBlock color="#256256">
<ExternalLinkBlock>
{game.itch && (
<ExternalLink href={game.itch.href}>View on itch.io</ExternalLink>
)}
</ExternalLinkBlock>
</ContentBlock>
</>
);
}

View file

@ -0,0 +1,56 @@
"use client";
import { styled } from "styled-components";
import Image, { StaticImageData } from "next/image";
export function Screenshots({
screenshots,
}: {
screenshots: StaticImageData[];
}) {
return (
<ScreenshotsWrapper>
{screenshots.map((screenshot) => (
<Screenshot src={screenshot} alt="" key={screenshot.src} />
))}
</ScreenshotsWrapper>
);
}
const ScreenshotsWrapper = styled.div`
flex: 4;
text-align: center;
`;
const Screenshot = styled(Image)`
width: 100%;
width: max(
round(down, 100%, calc(240 * var(--device-pixel))),
calc(240 * var(--device-pixel))
);
height: auto;
image-rendering: pixelated;
`;
export const Description = styled.div`
flex: 5;
:first-child {
margin-top: 0;
}
`;
export const DescriptionAndScreenshots = styled.div`
display: flex;
gap: 16px;
@media (max-width: 1000px) {
display: block;
}
`;
export const BackToShowcaseWrapper = styled.div`
a {
text-decoration: none;
color: black;
}
`;

View file

@ -0,0 +1,30 @@
import { ShowcaseGame, shuffle } from "@/app/showcase/games";
import d1 from "./the-dungeon-puzzlers-lament-0.png";
import d2 from "./the-dungeon-puzzlers-lament-1.png";
const Screenshots = [d1, d2];
export const Dungeon: ShowcaseGame = {
name: "The Dungeon Puzzler's Lament",
developers: shuffle(["Corwin Kuiper", "Gwilym Inzani"]),
screenshots: Screenshots,
description: (
<>
<p>
Get through as many levels as possible in this space themed, dice
rolling roguelike.
</p>
<p>
Build up powerful combos to defeat enemies which keep getting stronger.
Slowly acquire more dice and upgrade them in order to handle the
increasing strength of the enemies you face.
</p>
<p>
Hyperspace Roll was influenced by great games such as Slay the Spire,
FTL and the board game Escape: The Curse of the Temple.
</p>
</>
),
itch: new URL("https://setsquare.itch.io/dungeon-puzzlers-lament"),
};

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.9 KiB

View file

@ -0,0 +1,43 @@
import { ShowcaseGame, shuffle } from "../../../games";
import h1 from "./the-hat-chooses-the-wizard-0.png";
import h2 from "./the-hat-chooses-the-wizard-1.png";
import h3 from "./the-hat-chooses-the-wizard-2.png";
import h4 from "./the-hat-chooses-the-wizard-3.png";
const HatWizScreenshots = [h1, h2, h3, h4];
export const HatWiz: ShowcaseGame = {
name: "The Hat Chooses the Wizard",
developers: shuffle(["Corwin Kuiper", "Gwilym Inzani"]),
screenshots: HatWizScreenshots,
description: (
<>
<p>
&lsquo;The Hat Chooses the Wizard&rsquo; is a 2D platformer. This game
was developed as an entry for the GMTK game jam 2021, with the theme
&ldquo;joined together&rdquo;. The entire game, except for the music,
was produced in just 48 hours.
</p>
<p>
In this game, you play as a wizard searching for his missing staff.
However, the path to the staff is filled with dangerous obstacles and
monsters. Luckily, you have a powerful magic hat that can be thrown and
recalled, allowing you to fly towards it and reach otherwise
inaccessible platforms.
</p>
<p>
With this unique mechanic, you can explore the game&apos;s levels and
defeat enemies. The game&apos;s simple but challenging gameplay will put
your platforming skills to the test as you try to reach the end.
</p>
<p>
The music is by Otto Halmén released under creative commons attribution
3.0 and can be found here:{" "}
<a href="https://opengameart.org/content/sylvan-waltz-standard-looped-version">
opengameart.org/content/sylvan-waltz-standard-looped-version
</a>
</p>
</>
),
itch: new URL("https://lostimmortal.itch.io/the-hat-chooses-the-wizard"),
};

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

View file

@ -0,0 +1,30 @@
import { ShowcaseGame, shuffle } from "@/app/showcase/games";
import h1 from "./hyperspace-roll-0.png";
import h2 from "./hyperspace-roll-1.png";
const Screenshots = [h1, h2];
export const Hyperspace: ShowcaseGame = {
name: "Hyperspace Roll",
developers: shuffle(["Corwin Kuiper", "Gwilym Inzani", "Sam Williams"]),
screenshots: Screenshots,
description: (
<>
<p>
Get through as many levels as possible in this space themed, dice
rolling roguelike.
</p>
<p>
Build up powerful combos to defeat enemies which keep getting stronger.
Slowly acquire more dice and upgrade them in order to handle the
increasing strength of the enemies you face.
</p>
<p>
Hyperspace Roll was influenced by great games such as Slay the Spire,
FTL and the board game Escape: The Curse of the Temple.
</p>
</>
),
itch: new URL("https://lostimmortal.itch.io/hyperspace-roll"),
};

View file

@ -0,0 +1,26 @@
import { ShowcaseGame, shuffle } from "@/app/showcase/games";
import p1 from "./the-purple-night-0.png";
import p2 from "./the-purple-night-1.png";
const Screenshots = [p1, p2];
export const Purple: ShowcaseGame = {
name: "The Purple Night",
developers: shuffle(["Corwin Kuiper", "Gwilym Inzani", "Sam Williams"]),
screenshots: Screenshots,
description: (
<>
<p>Save a lost soul and take them safely back to the afterlife!</p>
<p>
The purple night is a platformer game where your health bar is your
sword. The more damage you take, the shorter your sword gets, making you
more nimble and your attacks faster, but also increasing your risk.
</p>
<p>
Do you choose to stay at high health but low mobility, or low health and
higher mobility?
</p>
</>
),
itch: new URL("https://lostimmortal.itch.io/the-purple-night"),
};

Binary file not shown.

After

Width:  |  Height:  |  Size: 2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

View file

@ -0,0 +1,34 @@
import { StaticImageData } from "next/image";
import { ReactNode } from "react";
import { HatWiz } from "./data/tapir/hatwiz/hatwiz";
import { Purple } from "./data/tapir/purple/purple";
import { Hyperspace } from "./data/tapir/hyperspace/hyperspace";
import { Dungeon } from "./data/tapir/dungeon/dungeon";
export interface ShowcaseGame {
name: string;
developers: string[];
rom?: URL;
screenshots: StaticImageData[];
description: ReactNode;
itch?: URL;
otherLink?: URL;
}
export function shuffle<T>(a: T[]) {
var j, x, i;
for (i = a.length - 1; i > 0; i--) {
j = Math.floor(Math.random() * (i + 1));
x = a[i];
a[i] = a[j];
a[j] = x;
}
return a;
}
export const Games: ShowcaseGame[] = shuffle([
HatWiz,
Purple,
Hyperspace,
Dungeon,
]);

View file

@ -0,0 +1,39 @@
import { Metadata } from "next";
import { ContentBlock } from "@/components/contentBlock";
import { Games, ShowcaseGame } from "./games";
import { slugify } from "@/sluggify";
import { GameDisplay, GameGrid, GameImage } from "./styles";
export const metadata: Metadata = {
title: "Showcase - agb",
};
export default function ColourPickerPage() {
return (
<>
<ContentBlock color="#AAAFFF">
<h1>Showcase</h1>
</ContentBlock>
<ContentBlock uncentered>
<GameGrid>
{Games.map((game, idx) => (
<Game key={idx} game={game} />
))}
</GameGrid>
</ContentBlock>
</>
);
}
function Game({ game }: { game: ShowcaseGame }) {
const showcaseImage = game.screenshots[game.screenshots.length - 1];
return (
<GameDisplay
href={`./showcase/${slugify(game.name)}`}
id={slugify(game.name)}
>
<GameImage src={showcaseImage} alt={`Screenshot of ${game.name}`} />
<h2>{game.name}</h2>
</GameDisplay>
);
}

View file

@ -0,0 +1,34 @@
"use client";
import Link from "next/link";
import styled from "styled-components";
import Image from "next/image";
export const GameGrid = styled.div`
display: grid;
grid-template-columns: repeat(auto-fit, min(100vw, 600px));
justify-content: center;
gap: 48px;
`;
export const GameImage = styled(Image)`
width: 100%;
width: max(
round(down, 100%, calc(240 * var(--device-pixel))),
min(calc(240 * var(--device-pixel)), 100vw)
);
height: auto;
image-rendering: pixelated;
`;
export const GameDisplay = styled(Link)`
width: 100%;
text-align: center;
color: black;
text-decoration: none;
h2 {
margin: 0;
margin-top: 8px;
}
`;

View file

@ -14,11 +14,8 @@ const Section = styled.section<{ $color: string }>`
const CENTERED_CSS = `
margin-left: auto;
margin-right: auto;
max-width: 60%;
@media (max-width: 40rem) {
max-width: 90%;
}
width: 60%;
min-width: min(95%, 1000px);
`;
export const CenteredBlock = styled.div`

View file

@ -0,0 +1,23 @@
"use client";
import Link from "next/link";
import { styled } from "styled-components";
export const ExternalLink = styled(Link)`
text-decoration: none;
color: black;
background-color: #fad288;
border: solid #fad288 2px;
border-radius: 5px;
padding: 5px 10px;
&:hover {
border: solid black 2px;
}
`;
export const ExternalLinkBlock = styled.div`
display: flex;
flex-wrap: wrap;
gap: 16px;
justify-content: space-around;
`;

View file

Before

Width:  |  Height:  |  Size: 163 B

After

Width:  |  Height:  |  Size: 163 B

View file

Before

Width:  |  Height:  |  Size: 173 B

After

Width:  |  Height:  |  Size: 173 B

View file

Before

Width:  |  Height:  |  Size: 190 B

After

Width:  |  Height:  |  Size: 190 B

View file

Before

Width:  |  Height:  |  Size: 189 B

After

Width:  |  Height:  |  Size: 189 B

View file

Before

Width:  |  Height:  |  Size: 241 B

After

Width:  |  Height:  |  Size: 241 B

View file

Before

Width:  |  Height:  |  Size: 156 B

After

Width:  |  Height:  |  Size: 156 B

View file

@ -1,4 +1,4 @@
import { FC, useMemo, useState } from "react";
import { useMemo, useState } from "react";
import styled from "styled-components";
import Image from "next/image";
@ -8,8 +8,8 @@ import DPad from "./gba-parts/dpad.png";
import ABButtons from "./gba-parts/ab.png";
import Select from "./gba-parts/SELECT.png";
import Start from "./gba-parts/START.png";
import { GbaKey } from "./mgba/bindings";
import { MgbaHandle } from "./mgba/mgba";
import { GbaKey } from "../mgba/bindings";
import { MgbaHandle } from "../mgba/mgba";
const MobileControls = styled.div`
display: flex;

View file

@ -4,7 +4,7 @@ import debugInit, {
DebugFile,
InitOutput,
AddressInfo,
} from "./vendor/backtrace/backtrace";
} from "../vendor/backtrace/backtrace";
let agbDebug: Promise<InitOutput> | undefined;

View file

@ -0,0 +1,3 @@
export function slugify(x: string) {
return x.toLowerCase().split(" ").join("-");
}