some sweeping changes that at some level involves adding a showcase

This commit is contained in:
Corwin 2024-04-28 22:52:59 +01:00
parent 3d0de3535e
commit 6d7e6f934a
No known key found for this signature in database
42 changed files with 423 additions and 99 deletions

View file

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

View file

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

View file

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

View file

Before

Width:  |  Height:  |  Size: 181 B

After

Width:  |  Height:  |  Size: 181 B

View file

@ -1,30 +1,18 @@
"use client"; "use client";
import styled from "styled-components"; import styled from "styled-components";
import { CenteredBlock, ContentBlock } from "./contentBlock"; import { CenteredBlock, ContentBlock } from "../components/contentBlock";
import MgbaWrapper from "./mgba/mgbaWrapper"; import MgbaWrapper from "../components/mgba/mgbaWrapper";
import Image from "next/image"; import Image from "next/image";
import left from "./gba-parts/left.png"; import left from "./left.png";
import right from "./gba-parts/right.png"; import right from "./right.png";
import { MobileController } from "./mobileController"; import { MobileController } from "../components/mobileController/mobileController";
import { useMemo, useRef, useState } from "react"; import { useMemo, useRef, useState } from "react";
import { GbaKey } from "./mgba/bindings"; import { GbaKey } from "../components/mgba/bindings";
import { useClientValue } from "./useClientValue.hook"; import { useClientValue } from "../hooks/useClientValue.hook";
import { MgbaHandle } from "./mgba/mgba"; import { MgbaHandle } from "../components/mgba/mgba";
import { ExternalLink } from "@/components/externalLink";
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` const HelpLinks = styled.div`
display: flex; display: flex;
@ -78,7 +66,7 @@ function shouldStartPlaying(isTouchScreen: boolean | undefined) {
return !isTouchScreen; 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() { function MgbaWithControllerSides() {
const mgba = useRef<MgbaHandle>(null); const mgba = useRef<MgbaHandle>(null);
@ -136,13 +124,13 @@ function MgbaWithControllerSides() {
export default function Home() { export default function Home() {
return ( return (
<> <>
<ContentBlock> <ContentBlock color="#AAAFFF">
<h1>agb - a rust framework for making Game Boy Advance games</h1> <h1>agb - a rust framework for making Game Boy Advance games</h1>
</ContentBlock> </ContentBlock>
<ContentBlock uncentered> <ContentBlock uncentered>
<MgbaWithControllerSides /> <MgbaWithControllerSides />
</ContentBlock> </ContentBlock>
<ContentBlock color="#f5755e"> <ContentBlock color="#256256">
<HelpLinks> <HelpLinks>
<ExternalLink href="https://github.com/agbrs/agb"> <ExternalLink href="https://github.com/agbrs/agb">
GitHub GitHub
@ -151,6 +139,7 @@ export default function Home() {
<ExternalLink href="https://docs.rs/agb/latest/agb/"> <ExternalLink href="https://docs.rs/agb/latest/agb/">
Docs Docs
</ExternalLink> </ExternalLink>
<ExternalLink href="./showcase">Showcase</ExternalLink>
</HelpLinks> </HelpLinks>
</ContentBlock> </ContentBlock>
</> </>

View file

Before

Width:  |  Height:  |  Size: 206 B

After

Width:  |  Height:  |  Size: 206 B

View file

@ -0,0 +1,93 @@
import { slugify } from "@/sluggify";
import { Games, ShowcaseGame } from "../games";
import { ContentBlock } from "@/components/contentBlock";
import { ExternalLink } from "@/components/externalLink";
import Link from "next/link";
import Image, { StaticImageData } from "next/image";
import {
ScreenshotsWrapper,
ScreenshotWrapper,
BackToShowcaseWrapper,
DescriptionAndScreenshots,
Description,
} from "./styled";
export async function generateStaticParams() {
return Games.map((game) => ({
game: slugify(game.name),
}));
}
export 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 Screenshots({ screenshots }: { screenshots: StaticImageData[] }) {
return (
<ScreenshotsWrapper>
{screenshots.map((screenshot) => (
<ScreenshotWrapper key={screenshot.src}>
<Image src={screenshot} alt="" />
</ScreenshotWrapper>
))}
</ScreenshotsWrapper>
);
}
function Display({ game }: { game: ShowcaseGame }) {
return (
<>
<ContentBlock color="#AAAFFF">
<BackToShowcaseWrapper>
<Link href="../showcase">
<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">
{game.itch && (
<ExternalLink href={game.itch.href}>View on itch.io</ExternalLink>
)}
</ContentBlock>
</>
);
}

View file

@ -0,0 +1,36 @@
"use client";
import { styled } from "styled-components";
export const ScreenshotsWrapper = styled.div`
flex: 4;
`;
export const ScreenshotWrapper = styled.div`
text-align: center;
img {
width: 100%;
width: round(down, 100%, 240px);
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;
`;
export const BackToShowcaseWrapper = styled.div`
a {
text-decoration: none;
color: black;
}
`;

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

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,27 @@
import { StaticImageData } from "next/image";
import { ReactNode } from "react";
import { HatWiz } from "./data/tapir/hatwiz/hatwiz";
import { Purple } from "./data/tapir/purple/purple";
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[] = [HatWiz, Purple];

View file

@ -0,0 +1,42 @@
import { Metadata } from "next";
import { ContentBlock } from "@/components/contentBlock";
import { Games, ShowcaseGame } from "./games";
import Link from "next/link";
import { slugify } from "@/sluggify";
import { GameDisplay, GameGrid, GameImage } from "./styles";
import Image from "next/image";
export const metadata: Metadata = {
title: "Games made with 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 lastImage = game.screenshots[game.screenshots.length - 1];
return (
<GameDisplay>
<Link href={`./showcase/${slugify(game.name)}`}>
<GameImage>
<Image src={lastImage} alt={`Screenshot of ${game.name}`} />
</GameImage>
<h2>{game.name}</h2>
</Link>
</GameDisplay>
);
}

View file

@ -0,0 +1,32 @@
"use client";
import styled from "styled-components";
export const GameGrid = styled.div`
display: flex;
flex-wrap: wrap;
justify-content: center;
`;
export const GameImage = styled.div`
img {
width: 100%;
width: round(down, 100%, 240px);
height: auto;
image-rendering: pixelated;
}
`;
export const GameDisplay = styled.div`
width: 600px;
a {
text-align: center;
color: black;
text-decoration: none;
}
h2 {
margin: 0;
margin-top: 8px;
}
`;

View file

@ -0,0 +1,16 @@
"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;
}
`;

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 styled from "styled-components";
import Image from "next/image"; import Image from "next/image";
@ -8,8 +8,8 @@ import DPad from "./gba-parts/dpad.png";
import ABButtons from "./gba-parts/ab.png"; import ABButtons from "./gba-parts/ab.png";
import Select from "./gba-parts/SELECT.png"; import Select from "./gba-parts/SELECT.png";
import Start from "./gba-parts/START.png"; import Start from "./gba-parts/START.png";
import { GbaKey } from "./mgba/bindings"; import { GbaKey } from "../mgba/bindings";
import { MgbaHandle } from "./mgba/mgba"; import { MgbaHandle } from "../mgba/mgba";
const MobileControls = styled.div` const MobileControls = styled.div`
display: flex; display: flex;

View file

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

View file

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