Compare commits

...

10 commits

Author SHA1 Message Date
de0b56ef5b
feat: commit unoptimised assets to cleanup later
Some checks are pending
Docker / run-tests (push) Waiting to run
Docker / build-and-push-image (push) Blocked by required conditions
2025-05-05 19:29:23 +01:00
26258249d1
Add un-optimised content warning to index 2025-04-28 21:11:06 +01:00
90edb8db0a
Add back a few videos to archive 2025-04-26 17:26:29 +01:00
522392cdfc
Make archive images openable in new tab 2025-04-26 16:59:32 +01:00
4bf6f04222
Publish partially complete changes in order to make applications 2025-04-26 16:20:40 +01:00
3698e926ea fix: remove trailing slash (#1)
Set `SERVER_REDIRECT_TRAILING_SLASH=false` within Dockerfile to disable SWS from adding a trailing slash.

Reviewed-on: https://code.troylusty.com/troy/troylusty.com/pulls/1
Co-authored-by: Troy <hello@troylusty.com>
Co-committed-by: Troy <hello@troylusty.com>
2025-04-14 01:16:25 +00:00
8eb41f8fa4
fix: remove duplicate dates on articles 2025-04-10 10:48:00 +01:00
78b0780e12
make cv a markdown page 2025-04-07 14:38:37 +01:00
a04446de76
chore: upgrade dependencies 2025-03-26 20:45:47 +00:00
82b68b9f11
fix: use CollectionEntry type where possible 2025-03-13 21:45:16 +00:00
231 changed files with 1429 additions and 1401 deletions

View file

@ -1,4 +1,4 @@
FROM node:latest as node
FROM node:latest AS node
USER node
WORKDIR /usr/src/app
@ -10,3 +10,4 @@ RUN ["npx", "astro", "build"]
FROM ghcr.io/static-web-server/static-web-server:latest
WORKDIR /
COPY --from=node /usr/src/app/dist /public
ENV SERVER_REDIRECT_TRAILING_SLASH=false

View file

@ -1,4 +1,4 @@
import { defineConfig } from "astro/config";
import { defineConfig, fontProviders } from "astro/config";
import sitemap from "@astrojs/sitemap";
import rehypeExternalLinks from "rehype-external-links";
import mdx from "@astrojs/mdx";
@ -31,4 +31,23 @@ export default defineConfig({
},
},
},
redirects: {
"/archive": "/projects/archive",
},
experimental: {
fonts: [
{
provider: fontProviders.fontsource(),
name: "Outfit",
cssVariable: "--font-outfit",
weights: ["100 900"],
},
{
provider: fontProviders.fontsource(),
name: "Red Hat Mono",
cssVariable: "--font-red-hat-mono",
weights: ["300 700"],
},
],
},
});

694
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -13,25 +13,23 @@
},
"dependencies": {
"@astrojs/check": "0.9.4",
"@astrojs/mdx": "^4.1.1",
"@astrojs/mdx": "^4.2.6",
"@astrojs/rss": "^4.0.11",
"@astrojs/sitemap": "3.2.1",
"@fontsource-variable/outfit": "^5.2.5",
"@fontsource-variable/red-hat-mono": "^5.2.5",
"@tailwindcss/vite": "^4.0.13",
"astro": "^5.4.3",
"@astrojs/sitemap": "3.3.1",
"@tailwindcss/vite": "^4.1.5",
"astro": "^5.7.10",
"astro-icon": "^1.1.5",
"rehype-external-links": "^3.0.0",
"tailwindcss": "^4.0.13",
"typescript": "^5.8.2"
"tailwindcss": "^4.1.5",
"typescript": "^5.8.3"
},
"devDependencies": {
"@iconify-json/mdi": "^1.2.3",
"@tailwindcss/typography": "^0.5.16",
"@types/node": "^22.13.10",
"@types/node": "^22.15.3",
"npm-check-updates": "^18.0.1",
"prettier": "^3.5.3",
"prettier-plugin-astro": "^0.14.1",
"prettier-plugin-tailwindcss": "^0.6.11",
"npm-check-updates": "^17.1.15"
"prettier-plugin-tailwindcss": "^0.6.11"
}
}

View file

@ -9,15 +9,28 @@ import RelatedArticles from "@components/RelatedArticles.astro";
const { article, isPost = false } = Astro.props;
const { Content } = await article.render();
let datesMatch = false;
if (article.data.date.getTime() == article.data.updated?.getTime()) {
datesMatch = true;
interface NameAndUrl {
name: string;
url: string;
}
const listFormatter = new Intl.ListFormat("en-GB", {
style: "long",
type: "conjunction",
});
function formatNamesWithLinks(namesAndUrls: NameAndUrl[]) {
const listFormatter = new Intl.ListFormat("en-GB", {
style: "long",
type: "conjunction",
});
const linkedNames = namesAndUrls.map((item: NameAndUrl) => {
return `<a href="${item.url}" rel="nofollow" target="_blank" class="underline hover:no-underline">${item.name}</a>`;
});
return listFormatter.format(linkedNames);
}
let formattedList: string | null = null;
if (article.data.extraAuthors && article.data.extraAuthors.length !== 0) {
formattedList = formatNamesWithLinks(article.data.extraAuthors);
}
---
<Layout
@ -41,7 +54,7 @@ const listFormatter = new Intl.ListFormat("en-GB", {
<div class="flex items-center gap-2">
<Icon name="mdi:calendar" />
{
datesMatch ? (
!article.data.updated ? (
<p title="Date">
<FormattedDate date={article.data.date} />
</p>
@ -70,9 +83,7 @@ const listFormatter = new Intl.ListFormat("en-GB", {
article.data.extraAuthors ? (
<div class="flex items-center gap-2">
<Icon name="mdi:account-plus" />
<p title="Collaborators">
{listFormatter.format(article.data.extraAuthors)}
</p>
<p title="Collaborators" set:html={formattedList} />
</div>
) : null
}
@ -80,7 +91,7 @@ const listFormatter = new Intl.ListFormat("en-GB", {
article.data.tags ? (
<div class="flex flex-wrap items-center gap-2">
<Icon name="mdi:tag" />
{article.data.tags.map((tag: any) => (
{article.data.tags.map((tag: string) => (
<a
href={`/tags/${createSlug(tag)}`}
class="underline hover:no-underline"

View file

@ -9,7 +9,7 @@ const { href, link } = Astro.props;
<a href={`${href}`}>
<div
class="bg-button text-secondary hover:bg-button-active flex w-fit flex-row items-center gap-1 justify-self-center rounded-full px-3 py-1.5 text-center text-sm font-medium text-nowrap capitalize transition-colors duration-300"
class="bg-button text-secondary hover:bg-button-active flex w-fit flex-row items-center gap-1 justify-self-center rounded-md px-3 py-1.5 text-center text-sm font-medium text-nowrap capitalize transition-colors duration-300"
>
{link}
</div>

View file

@ -0,0 +1,91 @@
---
import Link from "@components/Link.astro";
import { createSlug } from "@lib/utils";
const projects = [
{
id: 1,
name: "MUST FIND BEANS",
description:
"A fast-paced first person shooter set following the realization that youre all out of beans. The problem is, youre nearing the end of cooking all the other items and you cant just not have them. Without beans, the day just wont be started off right.",
tags: ["Godot", "Blender", "GIMP", "Steamworks"],
link: "/projects/must-find-beans",
done: false,
},
{
id: 2,
name: "troylusty.com",
description:
"My personal website made using Astro as a way to show off my portfolio of work and display blog posts.",
tags: [
"Astro",
"Tailwind CSS",
"TypeScript",
"Self-hosted Forgejo Actions",
"Docker",
],
link: "https://code.troylusty.com/troy/troylusty.com",
done: true,
},
{
id: 3,
name: "Artwork",
description:
"A collection of digital artwork created with a variety of tools.",
tags: ["Blender", "Cinema 4D", "DaVinci Resolve"],
link: "/projects",
done: true,
},
{
id: 4,
name: "Packard",
description:
"Packard is a simple terminal based RSS aggregator meant to allow you to take a quick glance at whats occurring in topics you care about.",
tags: ["Rust", "Tokio", "Clap", "NixOS Flake"],
link: "/projects/packard",
done: true,
},
];
const sortedProjects = [...projects].sort((a, b) => a.id - b.id);
---
<ol class="grid grid-cols-1 gap-3 pl-0 md:grid-cols-2 print:grid-cols-2">
{
sortedProjects.map((project) => (
<li class="flex flex-col overflow-hidden pl-0">
<div class="flex flex-col space-y-1.5">
<div class="space-y-1">
<>
<h3 class="mt-0 text-base font-semibold tracking-tight">
<Link
href={project.link}
class="text-secondary inline-flex items-center gap-1 hover:underline"
>
{project.done ? (
<span class="mr-1 h-1 w-1 rounded-full bg-green-500" />
) : (
<span class="mr-1 h-1 w-1 rounded-full bg-amber-500" />
)}
{project.name}
</Link>
</h3>
<p class="text-secondary/70 text-xs">{project.description}</p>
</>
</div>
</div>
<div class="mt-auto flex text-sm text-pretty">
<div class="mt-2 flex flex-wrap gap-1">
{project.tags.map((tag) => (
<a
href={`/tags/${createSlug(tag)}`}
class="bg-button text-secondary hover:bg-button-active flex w-fit flex-row items-center gap-1 justify-self-center rounded-sm px-2 py-1 text-center font-sans text-[10px] font-light text-nowrap capitalize no-underline transition-colors duration-300"
>
{tag}
</a>
))}
</div>
</div>
</li>
))
}
</ol>

View file

@ -0,0 +1,33 @@
---
import { Image } from "astro:assets";
import type { ImageMetadata } from "astro";
interface Item {
src: ImageMetadata;
alt: string;
}
interface Props {
items: Item[];
}
const { items } = Astro.props as Props;
---
<div class="grid grid-cols-1 gap-3 md:grid-cols-2">
{
items.map((item: Item) => (
<div class="flex-col overflow-hidden rounded-sm">
{item.src && (
<Image
src={item.src}
alt={item.alt}
title={item.alt}
loading="eager"
class="mt-0 mb-0 h-full max-h-[90svh] w-full object-cover"
/>
)}
</div>
))
}
</div>

View file

@ -2,6 +2,7 @@
import { SITE } from "@consts";
import gradient from "../../public/assets/gradient.avif";
import { ClientRouter } from "astro:transitions";
import { Font } from "astro:assets";
interface Props {
title: string;
@ -20,9 +21,6 @@ let { tags } = Astro.props;
if (typeof tags !== "undefined") {
tags = SITE.KEYWORDS.concat(tags);
}
import outfit from "@fontsource-variable/outfit/files/outfit-latin-wght-normal.woff2?url";
import redhatmono from "@fontsource-variable/red-hat-mono/files/red-hat-mono-latin-wght-normal.woff2?url";
---
<head>
@ -96,18 +94,6 @@ import redhatmono from "@fontsource-variable/red-hat-mono/files/red-hat-mono-lat
<ClientRouter />
<!-- Font Preload -->
<link
rel="preload"
as="font"
type="font/woff2"
href={outfit}
crossorigin="anonymous"
/>
<link
rel="preload"
as="font"
type="font/woff2"
href={redhatmono}
crossorigin="anonymous"
/>
<Font cssVariable="--font-outfit" preload />
<Font cssVariable="--font-red-hat-mono" preload />
</head>

View file

@ -5,26 +5,26 @@
<span class="text-nowrap">Troy Lusty</span>
<span class="text-tertiary text-pretty">Digital Designer</span>
</h1>
<div
<p
class="text-secondary/70 animate-reveal text-lg font-medium opacity-0 [animation-delay:0.1s]"
>
I am in my final year of studying on a Game Arts and Design BA (Hons) degree
and on the side I manage online operations for a family run
<a
href="/projects/camouflage-store"
class="underline decoration-2 underline-offset-2 hover:no-underline"
class="text-secondary underline decoration-2 underline-offset-2 hover:no-underline"
>outdoor apparel business</a
>.
</div>
<div
</p>
<p
class="text-tertiary animate-reveal text-lg font-medium opacity-0 [animation-delay:0.2s]"
>
Think I could help with a project you're working on?
<a
href="mailto:hello@troylusty.com"
class="ml-1 w-fit rounded-full bg-lime-500/20 px-2 py-1 text-sm text-lime-800 ring ring-lime-500 transition-colors duration-300 hover:bg-lime-500/30 dark:text-lime-200"
class="ml-1 w-fit rounded-sm bg-lime-500/20 px-2 py-1 text-sm text-lime-800 ring ring-lime-500 transition-colors duration-300 hover:bg-lime-500/30 dark:text-lime-200"
>
Send me a message
</a>
</div>
</p>
</section>

View file

@ -1,5 +1,5 @@
<div
class="prose prose-neutral dark:prose-invert prose-lg prose-img:max-h-[90svh] prose-img:rounded-sm prose-img:w-auto prose-img:max-w-full prose-video:max-h-[90svh] prose-video:rounded-sm prose-video:w-auto prose-video::max-w-full prose-a:hover:no-underline"
class="prose prose-neutral dark:prose-invert prose-lg prose-img:max-h-[90svh] prose-img:rounded-sm prose-img:w-auto prose-img:max-w-full prose-video:max-h-[90svh] prose-video:rounded-sm prose-video:w-auto prose-video::max-w-full prose-a:hover:no-underline prose-a:decoration-2 prose-a:underline-offset-2"
>
<slot />
</div>

View file

@ -19,7 +19,7 @@ const next = items[(index + 1) % items.length];
{
items.length > 1 ? (
<div class="animate-reveal mx-auto flex w-full flex-col-reverse items-center justify-between gap-4 opacity-0 [animation-delay:0.1s] md:flex-row">
<div class="animate-reveal mx-auto flex w-full flex-row flex-wrap items-center justify-between gap-2 opacity-0 [animation-delay:0.1s]">
<Button
href={`/${prev.collection}/${prev.slug}`}
link={`Previous: ${prev.data.title}`}

View file

@ -1,8 +1,10 @@
---
import FormattedDate from "@components/FormattedDate.astro";
import type { CollectionEntry } from "astro:content";
import { Icon } from "astro-icon/components";
type Props = {
collection: any;
collection: CollectionEntry<"posts" | "projects">;
};
const { collection } = Astro.props;
@ -18,10 +20,10 @@ const { collection } = Astro.props;
<h3 class="text-secondary mb-1 text-lg font-semibold text-nowrap">
{collection.data.title}
</h3>
<FormattedDate
date={collection.data.date}
className="text-tertiary block text-nowrap"
/>
<div class="text-tertiary flex items-center gap-2">
<Icon name="mdi:calendar" />
<FormattedDate date={collection.data.date} />
</div>
</div>
<p class="text-tertiary text-pretty">{collection.data.description}</p>
</article>

View file

@ -1,8 +1,9 @@
---
import { Image } from "astro:assets";
import type { CollectionEntry } from "astro:content";
type Props = {
collection: any;
collection: CollectionEntry<"projects">;
};
const { collection } = Astro.props;
@ -21,6 +22,13 @@ const { collection } = Astro.props;
fit="cover"
/>
</div>
<div
class="relative opacity-0 transition-all delay-100 duration-300 ease-in-out group-hover:opacity-100"
>
<p class="absolute right-5 bottom-5 font-medium">
{collection.data.title}
</p>
</div>
</a>
</article>
</li>

View file

@ -1,9 +1,10 @@
---
import { Image } from "astro:assets";
import type { CollectionEntry } from "astro:content";
interface Props {
images: CollectionEntry<"projects">[];
interval?: number;
images: any;
}
const { interval = 3000, images } = Astro.props;
@ -30,6 +31,11 @@ const { interval = 3000, images } = Astro.props;
class="h-full w-full rounded-sm object-cover transition-all duration-300 group-hover:brightness-50"
loading="eager"
/>
<div class="relative opacity-0 transition-all delay-100 duration-300 ease-in-out group-hover:opacity-100">
<p class="absolute right-5 bottom-5 font-medium">
{image.data.title}
</p>
</div>
</a>
</div>
))

View file

@ -34,6 +34,11 @@ export const SITE: Site = {
href: "/sitemap-index.xml",
icon: "mdi:sitemap",
},
{
name: "Curriculum vitae",
href: "/cv",
icon: "mdi:trophy",
},
{
name: "Email",
href: "mailto:hello@troylusty.com",
@ -59,10 +64,6 @@ export const SITE: Site = {
name: "Posts",
href: "/posts",
},
{
name: "CV",
href: "/cv",
},
],
};
@ -70,25 +71,6 @@ export const HOME: Metadata = {
TITLE: "Troy Lusty",
DESCRIPTION:
"Hi, my name is Troy and I'm a student 3D artist currently studying in my second year of an FdA Games and Interactive Design course in the UK.",
HOMESETTINGS: {
NUM_POSTS_ON_HOMEPAGE: 2,
NUM_PROJECTS_ON_HOMEPAGE: 6,
},
};
export const CV: Metadata = {
TITLE: "Troy Lusty",
DESCRIPTION: "Curriculum vitae.",
};
export const POSTS: Metadata = {
TITLE: "Posts",
DESCRIPTION: "A collection of articles on topics I am passionate about.",
};
export const WORK: Metadata = {
TITLE: "Work",
DESCRIPTION: "Where I have worked and what I have done.",
};
export const PROJECTS: Metadata = {
@ -96,3 +78,18 @@ export const PROJECTS: Metadata = {
DESCRIPTION:
"A collection of my projects, with links to repositories and demos.",
};
export const POSTS: Metadata = {
TITLE: "Posts",
DESCRIPTION: "A collection of articles on topics I am passionate about.",
};
export const ABOUT: Metadata = {
TITLE: "About",
DESCRIPTION: "About me.",
};
export const CV: Metadata = {
TITLE: "Troy Lusty",
DESCRIPTION: "Curriculum vitae.",
};

View file

@ -16,7 +16,14 @@ const posts = defineCollection({
alt: z.string(),
}),
tags: z.array(z.string()),
extraAuthors: z.array(z.string()).optional(),
extraAuthors: z
.array(
z.object({
name: z.string().min(1),
url: z.string().url(),
}),
)
.optional(),
categories: z.array(z.string()),
})
.merge(rssSchema),
@ -37,11 +44,18 @@ const projects = defineCollection({
alt: z.string(),
}),
tags: z.array(z.string()),
extraAuthors: z.array(z.string()).optional(),
extraAuthors: z
.array(
z.object({
name: z.string().min(1),
url: z.string().url(),
}),
)
.optional(),
categories: z.array(z.string()),
featured: z.boolean().optional(),
collection: z.boolean().optional(),
includeHero: z.boolean().optional(),
rank: z.number().positive().optional(),
})
.merge(rssSchema),
});

View file

@ -0,0 +1,212 @@
---
title: "Website"
date: 2025-04-25
description: "An overview of what I am using to host my digital content."
image:
url: "showcase.webp"
alt: "Website showcase"
categories: ["personal"]
tags: ["self-host", "forgejo", "docker", "vps"]
draft: true
---
This post will outline my workflow of using a self-hosted Forgejo instance and Actions runner to automatically deploy my personal site and any software releases, all without having to rely on another provider.
![Website showcase](showcase.webp)
## Steps
### Private image access login?
```sh
echo '<token>' | docker login code.troylusty.com -u troy --password-stdin
```
```sh
echo $(htpasswd -nB user) | sed -e s/\\$/\\$\\$/g
```
### Aliases for updating VPS and pruning Docker
```sh
echo 'alias dockerclean="docker system prune -a --volumes"' >> .bashrc
echo 'alias updateall="sudo apt update && sudo apt upgrade && sudo apt autoremove"' >> .bashrc
```
Thanks to [Tech Tales](https://tech-tales.blog/posts/2025/01-forgejo-runner-update) for the clean instructions on how to setup an Actions runner with Forgejo.
```sh
docker compose run --rm forgejo-runner 'forgejo-runner' 'generate-config' > forgejo-runner/config.yml
```
Setup `container.docker_host: "unix:///var/run/docker.sock"` and `container.network: "forgejo"` and any labels such as `runner.labels: ["ubuntu-latest:docker://gitea/runner-images:ubuntu-latest"]`
```
docker compose run --rm -it forgejo-runner 'forgejo-runner' 'register'
```
Input `http://forgejo:3000` as the domain since forgejo is the container name in Docker and port 3000 is its relevant port.
## Docker compose
```yaml
services:
traefik:
image: traefik:latest
container_name: traefik
command:
- "--providers.docker"
- "--providers.docker.exposedbydefault=false"
- "--entryPoints.websecure.address=:443"
- "--certificatesresolvers.myresolver.acme.tlschallenge=true"
- "--certificatesresolvers.myresolver.acme.email=traefik@troylusty.com"
- "--certificatesresolvers.myresolver.acme.storage=/letsencrypt/acme.json"
- "--entrypoints.web.address=:80"
- "--entrypoints.web.http.redirections.entrypoint.to=websecure"
- "--entrypoints.web.http.redirections.entrypoint.scheme=https"
- "--ping=true"
labels:
- "traefik.enable=true"
- "com.centurylinklabs.watchtower.enable=true"
- "traefik.http.middlewares.securityHeaders.headers.stsSeconds=31536000"
- "traefik.http.middlewares.securityHeaders.headers.stsIncludeSubdomains=true"
- "traefik.http.middlewares.securityHeaders.headers.frameDeny=true"
- "traefik.http.middlewares.securityHeaders.headers.contentTypeNosniff=true"
- "traefik.http.middlewares.securityHeaders.headers.contentSecurityPolicy=default-src 'self'; script-src 'self' 'unsafe-eval' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; base-uri 'none'; form-action 'self'; object-src 'none'; frame-ancestors 'none'; upgrade-insecure-requests"
- "traefik.http.middlewares.securityHeaders.headers.referrerPolicy=no-referrer"
- "traefik.http.middlewares.securityHeaders.headers.permissionsPolicy=accelerometer=(), autoplay=(), camera=(), cross-origin-isolated=(), display-capture=(), encrypted-media=(), fullscreen=(), geolocation=(), gyroscope=(), keyboard-map=(), magnetometer=(), microphone=(), midi=(), payment=(), picture-in-picture=(), publickey-credentials-get=(), screen-wake-lock=(), sync-xhr=(self), usb=(), web-share=(), xr-spatial-tracking=(), clipboard-read=(), clipboard-write=(), gamepad=(), hid=(), idle-detection=(), interest-cohort=(), serial=(), unload=()"
ports:
- "80:80"
- "443:443"
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro
- letsencrypt:/letsencrypt
restart: unless-stopped
networks:
- traefik
healthcheck:
test: ["CMD", "traefik", "healthcheck", "--ping"]
depends_on:
watchtower:
condition: service_healthy
watchtower:
image: containrrr/watchtower:latest
command: --label-enable --interval 1800 --rolling-restart --cleanup --remove-volumes
container_name: watchtower
networks:
- traefik
volumes:
- /var/run/docker.sock:/var/run/docker.sock
- /home/troy/.docker/config.json:/config.json
restart: unless-stopped
healthcheck:
test: ["CMD", "/watchtower", "--health-check"]
personalsite:
image: code.troylusty.com/troy/troylusty.com:latest
container_name: personalsite
labels:
- "traefik.enable=true"
- "traefik.http.routers.personalsite.rule=Host(`troylusty.com`)"
- "traefik.http.routers.personalsite.entrypoints=websecure"
- "traefik.http.routers.personalsite.tls.certresolver=myresolver"
- "com.centurylinklabs.watchtower.enable=true"
- "traefik.http.routers.personalsite.middlewares=securityHeaders"
restart: unless-stopped
networks:
- traefik
depends_on:
traefik:
condition: service_healthy
zolapress:
image: code.troylusty.com/troy/zolapress:latest
container_name: zolapress
profiles:
- donotstart
labels:
- "traefik.enable=true"
- "traefik.http.routers.zolapress.rule=Host(`edu.troylusty.com`)"
- "traefik.http.routers.zolapress.entrypoints=websecure"
- "traefik.http.routers.zolapress.tls.certresolver=myresolver"
- "com.centurylinklabs.watchtower.enable=true"
- "traefik.http.middlewares.auth.basicauth.users=troy:$$2y$$05$$fgVNzDsxXDq4co3aTh/OMOKZdLzUiM9XPEU5DXCivc9sYUZy/oq1W"
- "traefik.http.routers.zolapress.middlewares=securityHeaders, auth"
restart: unless-stopped
networks:
- traefik
depends_on:
traefik:
condition: service_healthy
unduck:
image: code.troylusty.com/troy/unduck:latest
container_name: unduck
labels:
- "traefik.enable=true"
- "traefik.http.routers.unduck.rule=Host(`unduck.troylusty.com`)"
- "traefik.http.routers.unduck.entrypoints=websecure"
- "traefik.http.routers.unduck.tls.certresolver=myresolver"
- "com.centurylinklabs.watchtower.enable=true"
- "traefik.http.routers.unduck.middlewares=securityHeaders"
restart: unless-stopped
networks:
- traefik
depends_on:
traefik:
condition: service_healthy
forgejo:
image: codeberg.org/forgejo/forgejo:10
container_name: forgejo
restart: unless-stopped
networks:
- traefik
- forgejo
labels:
- "traefik.enable=true"
- "traefik.http.routers.forgejo.rule=Host(`code.troylusty.com`)"
- "traefik.http.routers.forgejo.entrypoints=websecure"
- "traefik.http.routers.forgejo.tls.certresolver=myresolver"
- "com.centurylinklabs.watchtower.enable=true"
- "traefik.http.routers.forgejo.middlewares=securityHeaders"
- "traefik.http.services.forgejo.loadbalancer.server.port=3000"
- "traefik.docker.network=traefik"
volumes:
- ./forgejo:/data
ports:
- "2222:22"
depends_on:
traefik:
condition: service_healthy
forgejo-runner:
image: code.forgejo.org/forgejo/runner:6.0.1
container_name: forgejo-runner
user: 0:0
depends_on:
forgejo:
condition: service_started
networks:
- forgejo
labels:
- "com.centurylinklabs.watchtower.enable=true"
volumes:
- ./forgejo-runner:/data
- ./forgejo-runner/config.yml:/data/config.yml
- /var/run/docker.sock:/var/run/docker.sock
restart: unless-stopped
command: forgejo-runner -c /data/config.yml daemon
networks:
traefik:
external: false
name: traefik
forgejo:
external: false
name: forgejo
volumes:
letsencrypt:
```

Binary file not shown.

After

Width:  |  Height:  |  Size: 160 KiB

View file

@ -1,23 +0,0 @@
---
title: "3D Package Design"
description: "3D Package Design inspired by the work of Derek Elliott."
date: 2020-08-16
updated: 2020-08-16
image:
{ url: "troy-lusty-3d-package-design.avif", alt: "3D package design frame" }
tags: ["blender"]
categories: ["personal"]
---
import glowing_box_animation from "glowing-box-animation.webm";
3D Package Design inspired from [video](https://www.youtube.com/watch?v=4SRwODk0oOU) by Derek Elliott.
This was the final product from my first attempt at some simple animation within Blender done sometime in August of 2020.
<video preload="metadata" loop muted controls>
<source src={glowing_box_animation} type="video/webm" />
</video>
_Animation_
![3D package design frame](troy-lusty-3d-package-design.avif)

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 103 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 94 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 133 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 113 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 132 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 121 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 122 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 133 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.1 KiB

View file

@ -1,66 +1,70 @@
---
title: "A Long Way Down (Demo)"
title: "A Long Way Down"
description: "A short, atmospheric linear adventure created for my FdA Games and Interactive Design degree."
date: 2023-05-11
updated: 2023-05-11
featured: true
image: { url: "alwd-img1.avif", alt: "A Long Way Down Intro Showcase" }
image: { url: "alwd-img1.jpg", alt: "A Long Way Down Intro Showcase" }
tags: ["unreal engine", "blender", "inkscape"]
categories: ["education"]
includeHero: true
extraAuthors: ["Sam Griffiths"]
extraAuthors: [{ name: "Sam Griffiths", url: "https://samgriffiths.dev" }]
---
import Gallery from "@components/Gallery.astro";
import image1 from "alwd-img1.jpg";
import image2 from "alwd-img2.jpg";
import image3 from "alwd-img3.jpg";
import image4 from "alwd-img4.jpg";
import image5 from "alwd-img5.jpg";
import image6 from "alwd-img6.jpg";
import image7 from "29ff3-alwd-introarea-24042023.jpg";
import image8 from "Screenshot_20230402_222043.png";
import image9 from "Screenshot from 2023-03-07 15-30-43.png";
import image10 from "Screenshot from 2023-03-01 20-02-00.jpg";
import alongwaydown_demo_walkthrough from "alongwaydown-demo-walkthrough.webm";
A Long Way Down is a short, atmospheric linear adventure created alongside my friend [Sam](https://samgriffiths.dev) as a project for our FdA Games and Interactive Design degree. It is the follow up project to our previous work: [Nightmare](/projects/nightmare). Currently the [demo](https://samandtroy.itch.io/alongwaydown) is available on Itch.io.
A Long Way Down is a short, atmospheric linear adventure created alongside my friend Sam as a project for our FdA Games and Interactive Design degree. My role was art direction including the lighting, level design, and majority of asset creation for the project. This is the follow up project to our previous work: [Nightmare](#nightmare), and was made to be an evolutionary improvement of it. Currently the [demo](https://samandtroy.itch.io/alongwaydown) is available to download and play on Itch.io.
<video preload="metadata" controls>
<video preload="metadata" poster={image1.src} controls>
<source src={alongwaydown_demo_walkthrough} type="video/webm" />
</video>
_Demo walkthrough_
<Gallery
items={[
{ src: image1, alt: "A Long Way Down Intro Showcase" },
{ src: image2, alt: "A Long Way Down Forest Showcase" },
{ src: image3, alt: "A Long Way Down Tree Bridge Showcase" },
{ src: image4, alt: "A Long Way Down Swamp Showcase" },
{ src: image5, alt: "A Long Way Down Final Climb" },
{ src: image6, alt: "A Long Way Down Cliff Jump" },
{ src: image7, alt: "Alternate night lighting idea" },
{ src: image8, alt: "Old colour grade on forest section" },
{ src: image9, alt: "Early environment stage" },
{ src: image10, alt: "Initial style experiments" },
]}
/>
![A Long Way Down Intro Showcase](alwd-img1.avif)
## Nightmare
_Intro_
import image11 from "troy-lusty-nightmare.avif";
import image12 from "troy-lusty-nightmare-frame-1182.avif";
import image13 from "troy-lusty-highresscreenshot00000.png";
import image14 from "troy-lusty-untitled.png";
![A Long Way Down Forest Showcase](alwd-img2.avif)
import nightmare from "nightmare.webm";
_Forest_
This is the environment I created in collaboration with Sam Griffiths for our Unit 8 - Developing a Creative Media Production Project - Final Major Project which concluded 2021-04-21. I was in charge of the art side of the project including asset production, composition, and lighting. Additionally, I created this short cinematic in Unreal Engine's cinematic level sequencer by keyframing the transform values and focus distance of a camera.
![A Long Way Down Tree Bridge Showcase](alwd-img3.avif)
<video preload="metadata" muted controls>
<source src={nightmare} type="video/webm" />
</video>
_Tree bridge_
![A Long Way Down Swamp Showcase](alwd-img4.avif)
_Swamp_
![A Long Way Down Final Climb](alwd-img5.avif)
_Final climb_
![A Long Way Down Cliff Jump](alwd-img6.avif)
_Cliff jump_
![Alternate night lighting idea](alwd-alternate-night-lighting.avif)
_Alternate night lighting idea_
![Old colour grade on forest section](alwd-early-forest.avif)
_Old colour grade on forest section_
![Early environment stage](alwd-early-environment-stage.avif)
_Early environment stage_
![Initial style experiments](alwd-style-experiment.avif)
_Initial style experiments_
### External links
[Itch.io page](https://samandtroy.itch.io/alongwaydown)
<Gallery
items={[
{ src: image11, alt: "Frame 0600" },
{ src: image12, alt: "Frame 1182" },
{ src: image13, alt: "Development image 1" },
{ src: image14, alt: "Development image 2" },
]}
/>

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 MiB

View file

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 11 KiB

Before After
Before After

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 879 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 296 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 374 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 83 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 174 KiB

View file

Before

Width:  |  Height:  |  Size: 7.8 KiB

After

Width:  |  Height:  |  Size: 7.8 KiB

Before After
Before After

Binary file not shown.

After

Width:  |  Height:  |  Size: 199 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 314 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 329 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 808 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 191 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 494 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 306 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 MiB

After

Width:  |  Height:  |  Size: 8.9 MiB

Before After
Before After

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 MiB

View file

@ -0,0 +1,113 @@
---
title: "Archive"
description: "A collection of smaller, unfinished, or historic personal works."
date: 2021-01-15
featured: true
image: { url: "2023-11-23.jpg", alt: "Ugolino and His Sons lighting" }
tags:
[
"blender",
"unreal engine",
"davinci resolve",
"photoshop",
"gimp",
"affinity photo",
]
categories: ["personal"]
---
import Gallery from "@components/Gallery.astro";
Whilst not all of these pieces are large enough to have their own project page, I still think they showcase what I am interested in and it didn't feel right to exclude them entirely. This page receives updates periodically.
import video2023_04_08 from "./2023-04-08.webm";
import video2023_02_08 from "./2023-02-08.webm";
import video2023_01_08 from "./2023-01-08.webm";
import video2023_07_19 from "./2023-07-19.webm";
<div class="mb-6 grid grid-cols-1 gap-3 md:grid-cols-2">
<video
preload="metadata"
autoplay
muted
loop
class="mt-0 mb-0 aspect-square h-full max-h-[90svh] w-full object-cover"
>
<source src={video2023_04_08} type="video/webm" />
</video>
<video
preload="metadata"
autoplay
muted
loop
class="mt-0 mb-0 aspect-square h-full max-h-[90svh] w-full object-cover"
>
<source src={video2023_02_08} type="video/webm" />
</video>
<video
preload="metadata"
autoplay
muted
loop
class="mt-0 mb-0 aspect-square h-full max-h-[90svh] w-full object-cover"
>
<source src={video2023_01_08} type="video/webm" />
</video>
<video
preload="metadata"
autoplay
muted
loop
class="mt-0 mb-0 aspect-square h-full max-h-[90svh] w-full object-cover"
>
<source src={video2023_07_19} type="video/webm" />
</video>
</div>
import image2024_04_01 from "./2024-04-01.jpg";
import image2022_01_27 from "./2022-01-27.jpg";
import image2022_01_06 from "./2022-01-06.jpg";
import image2023_11_23 from "./2023-11-23.jpg";
import image2024_02_15 from "./2024-02-15.jpg";
import image2023_10_12 from "./2023-10-12.jpg";
import image2022_09_26 from "./2022-09-26.jpg";
import image2023_11_02 from "./2023-11-02.jpg";
import image2022_03_27 from "./2022-03-27.png";
import image2022_05_17 from "./2022-05-17.jpg";
import image2023_01_05 from "./2023-01-05.jpg";
import image2024_07_19 from "./2024-07-19.jpg";
import image2025_01_24 from "./2025-01-24.jpg";
import image2022_03_28 from "./2022-03-28.jpg";
import image2023_044_18 from "./2023-04-18.avif";
import imageforestfire from "./forestfire.jpg";
import imagedatsun from "./datsun.jpg";
import imageedit from "./edit.jpg";
import imagezomax from "./zomax.jpg";
import imagesea from "./sea.jpg";
import imagelovesongs from "./lovesongs-2-2153-P.jpg";
<Gallery
items={[
{ src: image2024_04_01, alt: "224 Torquay Road" },
{ src: image2022_01_27, alt: "Studying Spider" },
{ src: image2022_01_06, alt: "Firespline" },
{ src: image2023_11_23, alt: "Ugolino and His Sons lighting" },
{ src: image2024_02_15, alt: "Austin" },
{ src: image2023_10_12, alt: "Austin" },
{ src: image2022_09_26, alt: "Wes Cockx" },
{ src: image2023_11_02, alt: "Trees" },
{ src: image2022_03_27, alt: "Sewer" },
{ src: image2022_05_17, alt: "Hanging light fixtures" },
{ src: image2023_01_05, alt: "Digital Artefact: Corridor" },
{ src: image2024_07_19, alt: "DOF tiles" },
{ src: imageforestfire, alt: "Forest fire" },
{ src: image2025_01_24, alt: "Escalator" },
{ src: image2022_03_28, alt: "Astronaut" },
{ src: imagedatsun, alt: "Datsun" },
{ src: imagezomax, alt: "Zomax" },
{ src: imagesea, alt: "Sea" },
{ src: imagelovesongs, alt: "Love Songs record cover" },
{ src: image2023_044_18, alt: "Abstract swirl" },
{ src: imageedit, alt: "Little Nightmares" },
]}
/>

Binary file not shown.

After

Width:  |  Height:  |  Size: 805 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 545 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 67 KiB

View file

@ -1,23 +0,0 @@
---
title: "Astronaut"
description: "Lighting and camera test."
date: 2022-03-28
updated: 2022-03-28
image: { url: "troy-lusty-astronaut.avif", alt: "Astronaut final piece" }
tags: ["blender", "davinci resolve"]
categories: ["personal"]
---
Astronaut character lit with 1 area light using light nodes. Done to test lighting and the creation of a camera with an "anamorphic lens" inside of Blender. Final adjustments made within DaVinci Resolve including adding a lens dirt overlay by William Landgren.
![Astronaut final piece](troy-lusty-astronaut.avif)
_Astronaut Final Image_
### External links
[Domenico D&rsquo;Alisa&rsquo;s ArtStation](https://www.artstation.com/domenicodalisa)
[William Landgren&rsquo;s Instagram](https://www.instagram.com/landgrenwilliam)
[Astronaut asset](https://cubebrush.co/domenicodalisa/products/vsuspw/discovery-pay-what-you-want-2017)

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 103 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 33 KiB

View file

@ -6,12 +6,13 @@ date: 2022-10-18 # Date Persuit theme was purchased
updated: 2024-12-05
image:
{
url: "camouflage-store-barningham-raby.avif",
url: "t_pkD9Wf0QU-HD.jpg",
alt: "Camouflage Store YouTube video regarding Altberg boots comparison.",
}
tags: ["ecommerce", "shopify", "docker"]
categories: ["client work"]
featured: true
rank: 1
---
My role has me in charge of managing an [ecommerce store](https://camouflagestore.uk/) in addition to creating, editing, and publishing informational YouTube and social media content for a family run outdoors store. This includes the redesign shown below but also any maintenance and general upkeep of the site and all related systems.
@ -20,7 +21,7 @@ My role has me in charge of managing an [ecommerce store](https://camouflagestor
As of 2024-12-05 the [YouTube channel](https://www.youtube.com/@camouflagestoreuk) has 1.37k subscribers and 312,241 views over a total of 168 videos. If were to pick one video that displays the quality of the content we produce, it would probably be [SOLO ATP SAS SMOCK MK2 (2022) OVERVIEW | Camouflage Store](https://www.youtube.com/watch?v=K7wlm60rXVs). I am incredibly grateful to Steve for giving me the opportunity to continue working with him, and for the amount of creative freedom he gives me when experimenting with new ideas.
![SOLO ATP SAS SMOCK MK2 (2022) OVERVIEW | Camouflage Store YouTube Video Thumbnail](camouflage-store-video-thumbnail.avif)
![SOLO ATP SAS SMOCK MK2 (2022) OVERVIEW | Camouflage Store YouTube Video Thumbnail](K7wlm60rXVs-HD.jpg)
## Site redesign
@ -53,10 +54,3 @@ For the domain, we have gone with [camouflagestore.uk](https://camouflagestore.u
Continuing my goal of giving Steve the most amount of freedom possible without having to rely on thirdparty services, I have setup a VPS on his behalf to host a variety of services.
The first of which is an instance of [Umami](https://umami.is/), a self-hostable analytics platform. This has been hosted using Docker and includes automatic redeployments using Watchtower, and reverse proxying with Traefik.
### Other links
- [Camouflage Store](https://camouflagestore.uk)
- [YouTube](https://www.youtube.com/@camouflagestoreuk)
- [Instagram](https://www.instagram.com/camouflagestoreuk)
- [Twitter](https://twitter.com/camouflagestore)

Binary file not shown.

After

Width:  |  Height:  |  Size: 120 KiB

View file

@ -1,53 +0,0 @@
---
title: "Digital Artefact: Corridor (Incomplete)"
description: "A virtual production horror environment made in Unreal Engine 5 and inspired by The Shining."
date: 2023-01-20
updated: 2023-01-20
image:
{
url: "troy-lusty-highresscreenshot05012023-2.avif",
alt: "Progress 5 for Digital Artefact: Corridor project",
}
tags: ["unreal engine", "blender", "davinci resolve", "photoshop"]
categories: ["education"]
includeHero: true
---
import deltakey from "deltakey.webm";
import wallpaperpeel from "wallpaperpeel.webm";
The outcome I went into this project expecting was that I would produce an environment made entirely from scratch which I could create a short test virtual production shot in utilising a motion capture camera rig and live keying. Later I would then properly composite the two bits of footage together.
**This project is presented here in the state it was upon the university deadline. There were a couple issues that occurred towards the end of production which is why the project is listed as incomplete.**
![Progress 5 for Digital Artefact: Corridor project](troy-lusty-highresscreenshot05012023-2.avif)
_Using Lumen for global illumination._
![Progress 4 for Digital Artefact: Corridor project](troy-lusty-highresscreenshot05012023-1.avif)
_Set the project to use deprecated hardware raytracing instead of Lumen, which resulted in the light spill on the walls being fixed._
![Progress 3 for Digital Artefact: Corridor project](troy-lusty-highresscreenshot00001-squeeze.avif)
_First steps of migrating the scene over to Unreal Engine. Focusing on recreating the lights from Blender into Lumen. I am having issues softening up the shadows being cast from the light shade and wall mounting._
![Progress 2 for Digital Artefact: Corridor project](troy-lusty-hall-1205-2.avif)
_Modelled a new scene design by creating repeatable assets in Blender and utilising collection instancing._
![Progress 1 for Digital Artefact: Corridor project](troy-lusty-troy-lusty-output.avif)
_Initial idea made and presented in Blender for the project pitch._
<video preload="metadata" loop muted controls>
<source src={deltakey} type="video/webm" />
</video>
_Virtual production DaVinci Resolve delta key_
<video preload="metadata" loop muted controls>
<source src={wallpaperpeel} type="video/webm" />
</video>
_Wallpaper peel test_

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.4 KiB

View file

@ -1,15 +0,0 @@
---
title: "Discord Bot"
description: "AQA Computer Science NEA project based around creating a Discord bot."
date: 2020-03-31
updated: 2020-03-31
image: { url: "discord.avif", alt: "Discord bot" }
tags: ["python"]
categories: ["education"]
---
The objective I set myself was to write a Discord bot as my AQA Computer Science NEA Project. The program utilised [discord.py](https://github.com/Rapptz/discord.py), a Discord API wrapper for use within Python. The resulting code for the bot can be viewed in my Git repo.
### External links
https://code.troylusty.com/troy/discordbot

View file

@ -1,28 +0,0 @@
---
title: "Firespline"
description: "A fire animation test presented in a small cave scene."
date: 2022-01-06
updated: 2022-01-06
image: { url: "troy-lusty-firespline.avif", alt: "Firespline frame" }
tags: ["blender", "davinci resolve"]
categories: ["personal"]
---
import firespline from "firespline.webm";
Cycles and DaVinci Resolve (with assets from Blend Swap and ambientCG)
This is fire animation test I did which I turned into a scene complete with reflected light on water and volumetrics.
<video preload="metadata" loop muted controls>
<source src={firespline} type="video/webm" />
</video>
_Animation_
![Firespline frame](troy-lusty-firespline.avif)
_Extracted frame_
### Assets list
https://www.blendswap.com/blend/26395, https://ambientcg.com/view?id=Rock030, https://ambientcg.com/view?id=Ground033

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.3 KiB

View file

@ -1,40 +0,0 @@
---
title: "Kikimora"
description: "A narrative driven horror game prototype done as a university project."
date: 2024-01-24
image: { url: "kikimora-titlecard.avif", alt: "Kikimora titlecard" }
tags: ["godot", "blender", "gimp", "inkscape"]
categories: ["education"]
---
import kikimora_gameplay from "kikimora-gameplay.webm";
This was my first attempt at making anything within the Godot game engine, and was consequently what resulted in me starting the creation of [MUST FIND BEANS](https://store.steampowered.com/app/3012740/MUST_FIND_BEANS/).
### Controls
WASD - Movement
CTRL - Crouch
E - Interact
Mouse - Look around
<video preload="metadata" controls>
<source src={kikimora_gameplay} type="video/webm" />
</video>
_Short gameplay video_
![Kikimora hallway screenshot](kikimora-ingame-screenshot-1.avif)
_Screenshot 1_
![Kikimora red hallway screenshot](kikimora-ingame-screenshot-2.avif)
_Screenshot 2_
### External links
[Itch.io page](https://troylusty.itch.io/kikimora)

Some files were not shown because too many files have changed in this diff Show more