Start showcase pages (#657)
15
justfile
|
@ -98,18 +98,19 @@ 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"
|
||||||
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
|
setup-app-build: build-mgba-wasm build-combo-rom-site build-website-backtrace
|
||||||
|
|
|
@ -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>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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`
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import type { Metadata } from "next";
|
import type { Metadata } from "next";
|
||||||
import "./globalStyles.css";
|
import "./globalStyles.css";
|
||||||
import StyledComponentsRegistry from "./registry";
|
import StyledComponentsRegistry, { BodyPixelRatio } from "./registry";
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
title: "agb - a rust framework for making Game Boy Advance games",
|
title: "agb - a rust framework for making Game Boy Advance games",
|
||||||
|
@ -13,9 +13,9 @@ export default function RootLayout({
|
||||||
}>) {
|
}>) {
|
||||||
return (
|
return (
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<body>
|
<StyledComponentsRegistry>
|
||||||
<StyledComponentsRegistry>{children}</StyledComponentsRegistry>
|
<BodyPixelRatio>{children}</BodyPixelRatio>
|
||||||
</body>
|
</StyledComponentsRegistry>
|
||||||
</html>
|
</html>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
Before Width: | Height: | Size: 181 B After Width: | Height: | Size: 181 B |
|
@ -1,35 +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, ExternalLinkBlock } 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`
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-around;
|
|
||||||
`;
|
|
||||||
|
|
||||||
const GameDisplay = styled.div`
|
const GameDisplay = styled.div`
|
||||||
height: min(calc(100vw / 1.5), min(90vh, 480px));
|
height: min(calc(100vw / 1.5), min(90vh, 480px));
|
||||||
|
@ -78,7 +61,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,14 +119,14 @@ 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>
|
<ExternalLinkBlock>
|
||||||
<ExternalLink href="https://github.com/agbrs/agb">
|
<ExternalLink href="https://github.com/agbrs/agb">
|
||||||
GitHub
|
GitHub
|
||||||
</ExternalLink>
|
</ExternalLink>
|
||||||
|
@ -151,7 +134,8 @@ export default function Home() {
|
||||||
<ExternalLink href="https://docs.rs/agb/latest/agb/">
|
<ExternalLink href="https://docs.rs/agb/latest/agb/">
|
||||||
Docs
|
Docs
|
||||||
</ExternalLink>
|
</ExternalLink>
|
||||||
</HelpLinks>
|
<ExternalLink href="./showcase">Showcase</ExternalLink>
|
||||||
|
</ExternalLinkBlock>
|
||||||
</ContentBlock>
|
</ContentBlock>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import React, { useState } from "react";
|
import React, { useEffect, useState } from "react";
|
||||||
import { useServerInsertedHTML } from "next/navigation";
|
import { useServerInsertedHTML } from "next/navigation";
|
||||||
import { ServerStyleSheet, StyleSheetManager } from "styled-components";
|
import styled, { ServerStyleSheet, StyleSheetManager } from "styled-components";
|
||||||
|
|
||||||
export default function StyledComponentsRegistry({
|
export default function StyledComponentsRegistry({
|
||||||
children,
|
children,
|
||||||
|
@ -27,3 +27,18 @@ export default function StyledComponentsRegistry({
|
||||||
</StyleSheetManager>
|
</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>;
|
||||||
|
}
|
||||||
|
|
Before Width: | Height: | Size: 206 B After Width: | Height: | Size: 206 B |
81
website/agb/src/app/showcase/[game]/page.tsx
Normal 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><</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>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
56
website/agb/src/app/showcase/[game]/styles.tsx
Normal 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;
|
||||||
|
}
|
||||||
|
`;
|
30
website/agb/src/app/showcase/data/tapir/dungeon/dungeon.tsx
Normal 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"),
|
||||||
|
};
|
After Width: | Height: | Size: 5.8 KiB |
After Width: | Height: | Size: 7.9 KiB |
43
website/agb/src/app/showcase/data/tapir/hatwiz/hatwiz.tsx
Normal 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>
|
||||||
|
‘The Hat Chooses the Wizard’ is a 2D platformer. This game
|
||||||
|
was developed as an entry for the GMTK game jam 2021, with the theme
|
||||||
|
“joined together”. 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's levels and
|
||||||
|
defeat enemies. The game'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"),
|
||||||
|
};
|
After Width: | Height: | Size: 2.7 KiB |
After Width: | Height: | Size: 4.2 KiB |
After Width: | Height: | Size: 3.8 KiB |
After Width: | Height: | Size: 3.5 KiB |
After Width: | Height: | Size: 2.8 KiB |
After Width: | Height: | Size: 2.5 KiB |
|
@ -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"),
|
||||||
|
};
|
26
website/agb/src/app/showcase/data/tapir/purple/purple.tsx
Normal 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"),
|
||||||
|
};
|
After Width: | Height: | Size: 2 KiB |
After Width: | Height: | Size: 3.3 KiB |
34
website/agb/src/app/showcase/games.tsx
Normal 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,
|
||||||
|
]);
|
39
website/agb/src/app/showcase/page.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
34
website/agb/src/app/showcase/styles.tsx
Normal 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;
|
||||||
|
}
|
||||||
|
`;
|
|
@ -14,11 +14,8 @@ const Section = styled.section<{ $color: string }>`
|
||||||
const CENTERED_CSS = `
|
const CENTERED_CSS = `
|
||||||
margin-left: auto;
|
margin-left: auto;
|
||||||
margin-right: auto;
|
margin-right: auto;
|
||||||
max-width: 60%;
|
width: 60%;
|
||||||
|
min-width: min(95%, 1000px);
|
||||||
@media (max-width: 40rem) {
|
|
||||||
max-width: 90%;
|
|
||||||
}
|
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const CenteredBlock = styled.div`
|
export const CenteredBlock = styled.div`
|
23
website/agb/src/components/externalLink.tsx
Normal 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;
|
||||||
|
`;
|
Before Width: | Height: | Size: 163 B After Width: | Height: | Size: 163 B |
Before Width: | Height: | Size: 173 B After Width: | Height: | Size: 173 B |
Before Width: | Height: | Size: 190 B After Width: | Height: | Size: 190 B |
Before Width: | Height: | Size: 189 B After Width: | Height: | Size: 189 B |
Before Width: | Height: | Size: 241 B After Width: | Height: | Size: 241 B |
Before Width: | Height: | Size: 156 B After Width: | Height: | Size: 156 B |
|
@ -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;
|
|
@ -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;
|
||||||
|
|
3
website/agb/src/sluggify.ts
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
export function slugify(x: string) {
|
||||||
|
return x.toLowerCase().split(" ").join("-");
|
||||||
|
}
|